Add first-run setup wizard, README, remove default users, add easter egg

This commit is contained in:
InviteMe Developer
2026-08-31 13:27:32 +03:00
parent 391650cf88
commit 8120ab4c3c
7 changed files with 167 additions and 112 deletions
+77 -62
View File
@@ -1,88 +1,103 @@
# InviteMe # InviteMe
Приложение для регистрации гостей на корпоративные мероприятия. Аналог Timepad с упрощённым функционалом. Система регистрации гостей, рассылки приглашений и контроля входа на мероприятия.
## Возможности ## Возможности
- Управление пользователями (админ, организатор, сотрудник на входе) - Создание мероприятий и управление ими.
- Создание и управление несколькими мероприятиями - Импорт гостей из Excel.
- Загрузка списка гостей из Excel в рамках мероприятия - Генерация QR-кодов и персональных пригласительных билетов.
- Хранение фото гостей и привязка по названию файла - Конструктор билетов с логотипом, цветами и предпросмотром.
- Генерация персональных QR-кодов - Рассылка приглашений по SMTP, EWS (Exchange) или через локальный Outlook.
- Автоматическая рассылка приглашений на email - Проверка приглашений на входе (сканирование QR / поиск по списку).
- Ручное добавление/удаление/блокировка участников - Ролевая модель: администратор, организатор, стафф.
- Проверка входа по QR-коду или поиску по ФИО - Аудит действий пользователей.
- Аналитика посещаемости для администратора - Поддержка Docker.
- Выгрузка отчётов в Excel
## Стек
- Python 3.10+
- Flask + Flask-SQLAlchemy
- SQLite
- Bootstrap 5
- html5-qrcode
- SMTP
## Быстрый старт ## Быстрый старт
### Вариант 1: Docker (рекомендуется)
Требования: установленный Docker и Docker Compose.
```bash ```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 python -m venv venv
venv\Scripts\activate # Windows:
pip install -r requirements.txt 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/ InviteMe/
├── app/ ├── app/ # Flask-приложение
│ ├── __init__.py │ ├── services/ # QR, билеты, почта, Excel
│ ├── models.py # Модели User, Event, Guest и CheckIn │ ├── templates/ # HTML-шаблоны
│ ├── routes.py # Все маршруты приложения │ ├── static/ # CSS, JS, изображения
│ ├── services/ # QR, email, Excel │ └── utils/ # Вспомогательные утилиты
│ ├── static/ ├── instance/ # SQLite-база (не в git)
│ │ ├── css/ ├── app/static/uploads/ # Загруженные файлы (не в git)
│ │ ├── js/ ├── Dockerfile
│ │ └── uploads/ # Фото и QR-коды ├── docker-compose.yml
│ └── templates/ └── requirements.txt
│ ├── admin/
│ ├── checkin/
│ └── email/
├── config.py # Конфигурация
├── docs/
│ └── DEPLOY.md # Подробная инструкция по развёртыванию
├── requirements.txt
├── run.py
└── README.md
``` ```
## Контекст проекта ## Лицензия
Проект разрабатывается для компании **Integrita**. Планируется развёртывание на виртуальной машине. Проект распространяется как есть для внутреннего использования.
- Подробный контекст, бизнес-процесс и чеклист — в `docs/CONTEXT.md`.
- Инструкции по настройке SMTP, запуску в production и резервному копированию — в `docs/DEPLOY.md`.
## Ссылки на документацию
- `docs/CONTEXT.md` — контекст, роли, поток данных, чеклист
- `docs/DEPLOY.md` — развёртывание, настройка SMTP, запуск в production
-35
View File
@@ -36,7 +36,6 @@ def create_app(config_class=Config):
with app.app_context(): with app.app_context():
db.create_all() db.create_all()
_migrate_existing_data(app) _migrate_existing_data(app)
_create_default_users(app)
_load_smtp_settings(app) _load_smtp_settings(app)
return app return app
@@ -197,37 +196,3 @@ def _migrate_existing_data(app):
db.session.rollback() db.session.rollback()
logger.warning(f"Migration skipped or failed: {e}") 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}")
+46
View File
@@ -147,9 +147,52 @@ def _guest_matches_search(guest, query):
# Auth # 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"]) @main_bp.route("/login", methods=["GET", "POST"])
@limiter.limit("10 per minute", methods=["POST"]) @limiter.limit("10 per minute", methods=["POST"])
def login(): def login():
if _is_first_run():
return redirect(url_for("main.setup"))
if request.method == "POST": if request.method == "POST":
username = request.form.get("username", "").strip() username = request.form.get("username", "").strip()
password = request.form.get("password", "").strip() password = request.form.get("password", "").strip()
@@ -1341,6 +1384,9 @@ def api_guest_detail(token):
@main_bp.route("/") @main_bp.route("/")
def index(): def index():
if _is_first_run():
return redirect(url_for("main.setup"))
user = current_user() user = current_user()
if not user: if not user:
return redirect(url_for("main.login")) return redirect(url_for("main.login"))
+5
View File
@@ -7,6 +7,7 @@
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet"> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/css/bootstrap.min.css" rel="stylesheet">
<link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}"> <link rel="stylesheet" href="{{ url_for('static', filename='css/style.css') }}">
<link rel="icon" type="image/png" href="{{ url_for('static', filename='images/inviteme_logo.png') }}"> <link rel="icon" type="image/png" href="{{ url_for('static', filename='images/inviteme_logo.png') }}">
<!-- InviteMe — crafted for mycloud24.ru -->
</head> </head>
<body> <body>
<nav class="navbar navbar-expand-lg navbar-dark"> <nav class="navbar navbar-expand-lg navbar-dark">
@@ -79,5 +80,9 @@
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.2/dist/js/bootstrap.bundle.min.js"></script>
{% block scripts %}{% endblock %} {% block scripts %}{% endblock %}
<footer class="mt-auto py-4 text-center">
<small class="text-muted" style="opacity: 0.25; font-size: 0.7rem;">mycloud24.ru</small>
</footer>
</body> </body>
</html> </html>
+37
View File
@@ -0,0 +1,37 @@
{% extends "base.html" %}
{% block title %}Первоначальная настройка — InviteMe{% endblock %}
{% block content %}
<div class="row justify-content-center align-items-center" style="min-height: 70vh;">
<div class="col-md-5">
<div class="card mt-5">
<div class="card-body p-4">
<div class="text-center mb-4">
<img src="{{ url_for('static', filename='images/inviteme_logo.png') }}" alt="InviteMe" class="login-logo">
</div>
<h3 class="card-title text-center mb-3" style="color: var(--accent-color); text-shadow: 0 0 12px var(--accent-glow);">Добро пожаловать</h3>
<p class="text-muted text-center mb-4">
Создайте первого администратора, чтобы начать работу с InviteMe.
</p>
<form method="POST" action="{{ url_for('main.setup') }}">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="mb-3">
<label for="username" class="form-label">Логин администратора</label>
<input type="text" class="form-control" id="username" name="username" required autofocus>
</div>
<div class="mb-3">
<label for="password" class="form-label">Пароль</label>
<input type="password" class="form-control" id="password" name="password" required>
</div>
<div class="mb-4">
<label for="password_confirm" class="form-label">Повторите пароль</label>
<input type="password" class="form-control" id="password_confirm" name="password_confirm" required>
</div>
<button type="submit" class="btn btn-primary w-100">Создать администратора</button>
</form>
</div>
</div>
</div>
</div>
{% endblock %}
+1 -9
View File
@@ -28,15 +28,7 @@ class Config:
MAIL_USERNAME = os.environ.get("MAIL_USERNAME") or "" MAIL_USERNAME = os.environ.get("MAIL_USERNAME") or ""
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") or "" MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") or ""
MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER") or MAIL_USERNAME MAIL_DEFAULT_SENDER = os.environ.get("MAIL_DEFAULT_SENDER") or MAIL_USERNAME
# Admin credentials
ADMIN_USERNAME = os.environ.get("ADMIN_USERNAME") or "admin"
ADMIN_PASSWORD = os.environ.get("ADMIN_PASSWORD") or "admin"
# Staff credentials (for check-in devices)
STAFF_USERNAME = os.environ.get("STAFF_USERNAME") or "staff"
STAFF_PASSWORD = os.environ.get("STAFF_PASSWORD") or "staff"
# Default event name for migrating existing guests # Default event name for migrating existing guests
DEFAULT_EVENT_NAME = os.environ.get("DEFAULT_EVENT_NAME") or "Архивное мероприятие" DEFAULT_EVENT_NAME = os.environ.get("DEFAULT_EVENT_NAME") or "Архивное мероприятие"
+1 -6
View File
@@ -8,15 +8,10 @@ services:
ports: ports:
- "8888:8888" - "8888:8888"
environment: environment:
# Замени на сильный случайный ключ # Замени на сильный случайный ключ перед запуском в продакшене
- SECRET_KEY=change-me-in-production - SECRET_KEY=change-me-in-production
- BASE_URL=https://ai.mycloud24.ru - BASE_URL=https://ai.mycloud24.ru
- TIMEZONE=Europe/Moscow - TIMEZONE=Europe/Moscow
# Первоначальные учётные записи (только при первом запуске)
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=change-admin-password
- STAFF_USERNAME=staff
- STAFF_PASSWORD=change-staff-password
volumes: volumes:
# База SQLite и загруженные файлы хранятся вне контейнера # База SQLite и загруженные файлы хранятся вне контейнера
- ./instance:/app/instance - ./instance:/app/instance