diff --git a/README.md b/README.md index 83e0ca0..6e9cf06 100644 --- a/README.md +++ b/README.md @@ -1,88 +1,103 @@ # InviteMe -Приложение для регистрации гостей на корпоративные мероприятия. Аналог Timepad с упрощённым функционалом. +Система регистрации гостей, рассылки приглашений и контроля входа на мероприятия. ## Возможности -- Управление пользователями (админ, организатор, сотрудник на входе) -- Создание и управление несколькими мероприятиями -- Загрузка списка гостей из Excel в рамках мероприятия -- Хранение фото гостей и привязка по названию файла -- Генерация персональных QR-кодов -- Автоматическая рассылка приглашений на email -- Ручное добавление/удаление/блокировка участников -- Проверка входа по QR-коду или поиску по ФИО -- Аналитика посещаемости для администратора -- Выгрузка отчётов в Excel - -## Стек - -- Python 3.10+ -- Flask + Flask-SQLAlchemy -- SQLite -- Bootstrap 5 -- html5-qrcode -- SMTP +- Создание мероприятий и управление ими. +- Импорт гостей из Excel. +- Генерация QR-кодов и персональных пригласительных билетов. +- Конструктор билетов с логотипом, цветами и предпросмотром. +- Рассылка приглашений по SMTP, EWS (Exchange) или через локальный Outlook. +- Проверка приглашений на входе (сканирование QR / поиск по списку). +- Ролевая модель: администратор, организатор, стафф. +- Аудит действий пользователей. +- Поддержка Docker. ## Быстрый старт +### Вариант 1: Docker (рекомендуется) + +Требования: установленный Docker и Docker Compose. + ```bash -cd C:\Projects\Integrita\InviteMe +git clone ssh://git@192.168.1.101:2222/insight/inviteme.git +cd inviteme + +# Собрать и запустить +docker compose up -d --build + +# Приложение будет доступно по адресу http://localhost:8888 +``` + +При первом запуске откройте `http://localhost:8888` — вас встретит страница создания первого администратора. + +### Вариант 2: Запуск вручную + +Требования: Python 3.12. + +```bash +git clone ssh://git@192.168.1.101:2222/insight/inviteme.git +cd inviteme + python -m venv venv -venv\Scripts\activate -pip install -r requirements.txt +# Windows: +venv\Scripts\pip install -r requirements.txt +# Linux/macOS: +# venv/bin/pip install -r requirements.txt + +# Запуск +venv\Scripts\python run_waitress.py ``` -Создайте файл `.env` в корне проекта (см. `docs/DEPLOY.md`) и запустите: +Приложение запустится на `http://localhost:8888`. -```bash -flask --app run.py run --host=0.0.0.0 --port=5000 -``` +## Первый запуск -## Учётные записи по умолчанию +1. Откройте приложение в браузере. +2. На странице приветствия создайте первого администратора. +3. Войдите под созданной учётной записью. +4. Создайте мероприятие и настройте почту в разделе «Рассылка → Настройки почты». -При первом запуске автоматически создаются: +## Настройка почты -- Админ: `admin` / `admin` -- Сотрудник на входе: `staff` / `staff` +В разделе **Рассылка → Настройки почты** можно выбрать один из способов отправки: -После входа администратор может создавать новых пользователей и мероприятия через веб-интерфейс. +- **SMTP** — указать сервер, порт, логин и пароль. +- **EWS** — для Microsoft Exchange, указать URL вида `https://mail.company.com/EWS/Exchange.asmx`. +- **Локальный Outlook** — работает только если приложение запущено на Windows-компьютере с установленным Outlook. -> Обязательно смените пароли в production через переменные окружения или `.env`. +## Безопасность + +- Сразу после первого запуска задайте сильный `SECRET_KEY` через переменную окружения. +- Не храните реальные пароли в репозитории — используйте `.env` файл или переменные окружения. +- Папка `instance/` (SQLite) и `app/static/uploads/` не попадают в git. + +## Переменные окружения + +| Переменная | Описание | По умолчанию | +|------------|----------|--------------| +| `SECRET_KEY` | Секретный ключ для сессий | `dev-secret-key-change-in-production` | +| `BASE_URL` | Базовый URL приложения | — | +| `TIMEZONE` | Часовой пояс | `Europe/Moscow` | +| `DATABASE_URL` | URI базы данных (SQLite по умолчанию) | `sqlite:///instance/inviteme.db` | ## Структура проекта ``` InviteMe/ -├── app/ -│ ├── __init__.py -│ ├── models.py # Модели User, Event, Guest и CheckIn -│ ├── routes.py # Все маршруты приложения -│ ├── services/ # QR, email, Excel -│ ├── static/ -│ │ ├── css/ -│ │ ├── js/ -│ │ └── uploads/ # Фото и QR-коды -│ └── templates/ -│ ├── admin/ -│ ├── checkin/ -│ └── email/ -├── config.py # Конфигурация -├── docs/ -│ └── DEPLOY.md # Подробная инструкция по развёртыванию -├── requirements.txt -├── run.py -└── README.md +├── app/ # Flask-приложение +│ ├── services/ # QR, билеты, почта, Excel +│ ├── templates/ # HTML-шаблоны +│ ├── static/ # CSS, JS, изображения +│ └── utils/ # Вспомогательные утилиты +├── instance/ # SQLite-база (не в git) +├── app/static/uploads/ # Загруженные файлы (не в git) +├── Dockerfile +├── docker-compose.yml +└── requirements.txt ``` -## Контекст проекта +## Лицензия -Проект разрабатывается для компании **Integrita**. Планируется развёртывание на виртуальной машине. - -- Подробный контекст, бизнес-процесс и чеклист — в `docs/CONTEXT.md`. -- Инструкции по настройке SMTP, запуску в production и резервному копированию — в `docs/DEPLOY.md`. - -## Ссылки на документацию - -- `docs/CONTEXT.md` — контекст, роли, поток данных, чеклист -- `docs/DEPLOY.md` — развёртывание, настройка SMTP, запуск в production +Проект распространяется как есть для внутреннего использования. diff --git a/app/__init__.py b/app/__init__.py index 796f326..969413a 100644 --- a/app/__init__.py +++ b/app/__init__.py @@ -36,7 +36,6 @@ def create_app(config_class=Config): with app.app_context(): db.create_all() _migrate_existing_data(app) - _create_default_users(app) _load_smtp_settings(app) return app @@ -197,37 +196,3 @@ def _migrate_existing_data(app): db.session.rollback() logger.warning(f"Migration skipped or failed: {e}") - -def _create_default_users(app): - """Создание начальных пользователей, если в базе нет ни одного.""" - from app.models import User - - try: - if User.query.first() is not None: - return - - admin = User( - username=app.config.get("ADMIN_USERNAME", "admin"), - role="admin", - is_active=True, - ) - admin.set_password(app.config.get("ADMIN_PASSWORD", "admin")) - - staff = User( - username=app.config.get("STAFF_USERNAME", "staff"), - role="staff", - is_active=True, - ) - staff.set_password(app.config.get("STAFF_PASSWORD", "staff")) - - db.session.add(admin) - db.session.add(staff) - db.session.commit() - - if app.config.get("ADMIN_PASSWORD") == "admin" or app.config.get("STAFF_PASSWORD") == "staff": - logger.warning("Default users created with default passwords. Change them in production!") - else: - logger.info("Default users 'admin' and 'staff' created from environment variables.") - except Exception as e: - db.session.rollback() - logger.warning(f"Default users creation skipped or failed: {e}") diff --git a/app/routes.py b/app/routes.py index 6522321..dbff1ec 100644 --- a/app/routes.py +++ b/app/routes.py @@ -147,9 +147,52 @@ def _guest_matches_search(guest, query): # Auth # --------------------------- +def _is_first_run(): + """True, если в базе ещё нет ни одного пользователя.""" + return User.query.first() is None + + +@main_bp.route("/setup", methods=["GET", "POST"]) +@limiter.limit("10 per minute", methods=["POST"]) +def setup(): + """Первоначальная настройка: создание первого администратора.""" + if not _is_first_run(): + return redirect(url_for("main.login")) + + if request.method == "POST": + username = request.form.get("username", "").strip() + password = request.form.get("password", "").strip() + password_confirm = request.form.get("password_confirm", "").strip() + + if not username or not password: + flash("Логин и пароль обязательны", "danger") + return redirect(url_for("main.setup")) + + if password != password_confirm: + flash("Пароли не совпадают", "danger") + return redirect(url_for("main.setup")) + + if User.query.filter_by(username=username).first(): + flash("Пользователь с таким логином уже существует", "danger") + return redirect(url_for("main.setup")) + + admin = User(username=username, role="admin", is_active=True) + admin.set_password(password) + db.session.add(admin) + db.session.commit() + + flash("Первый администратор создан. Войдите в систему.", "success") + return redirect(url_for("main.login")) + + return render_template("setup.html") + + @main_bp.route("/login", methods=["GET", "POST"]) @limiter.limit("10 per minute", methods=["POST"]) def login(): + if _is_first_run(): + return redirect(url_for("main.setup")) + if request.method == "POST": username = request.form.get("username", "").strip() password = request.form.get("password", "").strip() @@ -1341,6 +1384,9 @@ def api_guest_detail(token): @main_bp.route("/") def index(): + if _is_first_run(): + return redirect(url_for("main.setup")) + user = current_user() if not user: return redirect(url_for("main.login")) diff --git a/app/templates/base.html b/app/templates/base.html index db50f51..b221a1a 100644 --- a/app/templates/base.html +++ b/app/templates/base.html @@ -7,6 +7,7 @@ +