Add first-run setup wizard, README, remove default users, add easter egg
This commit is contained in:
@@ -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
|
|
||||||
|
|||||||
@@ -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}")
|
|
||||||
|
|||||||
@@ -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"))
|
||||||
|
|||||||
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
@@ -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
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user