Add first-run setup wizard, README, remove default users, add easter egg
This commit is contained in:
@@ -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
|
||||
Проект распространяется как есть для внутреннего использования.
|
||||
|
||||
@@ -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}")
|
||||
|
||||
@@ -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"))
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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 %}
|
||||
@@ -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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user