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
Приложение для регистрации гостей на корпоративные мероприятия. Аналог 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
Проект распространяется как есть для внутреннего использования.
-35
View File
@@ -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}")
+46
View File
@@ -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"))
+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 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') }}">
<!-- InviteMe — crafted for mycloud24.ru -->
</head>
<body>
<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>
{% 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>
</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 %}
-8
View File
@@ -29,14 +29,6 @@ class Config:
MAIL_PASSWORD = os.environ.get("MAIL_PASSWORD") or ""
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 = os.environ.get("DEFAULT_EVENT_NAME") or "Архивное мероприятие"
+1 -6
View File
@@ -8,15 +8,10 @@ services:
ports:
- "8888:8888"
environment:
# Замени на сильный случайный ключ
# Замени на сильный случайный ключ перед запуском в продакшене
- SECRET_KEY=change-me-in-production
- BASE_URL=https://ai.mycloud24.ru
- TIMEZONE=Europe/Moscow
# Первоначальные учётные записи (только при первом запуске)
- ADMIN_USERNAME=admin
- ADMIN_PASSWORD=change-admin-password
- STAFF_USERNAME=staff
- STAFF_PASSWORD=change-staff-password
volumes:
# База SQLite и загруженные файлы хранятся вне контейнера
- ./instance:/app/instance