# Forbidden Stars — учёт партий Мобильное веб-приложение для учёта партий настольной игры **Forbidden Stars**: профили игроков (вход по логину и паролю или через Telegram), группы с приглашениями, создание партий с рандомом фракций и фильтром по дополнениям группы, фото партий, уведомления, статистика и общий топ, админ-панель. - **Бэкенд / ядро + API:** Python · FastAPI · SQLModel · SQLite - **Фронтенд:** React · Vite · TypeScript (SPA; с ядром общается по REST API, живые обновления приходят SSE-потоком `/api/events`) - **Хостинг:** Raspberry Pi 4 (ARM64) в Docker; наружу — через VPS-привратник (см. «Домен и публикация») ## Структура ``` backend/ FastAPI: ядро, REST API, БД, миграции Alembic, seed, тесты frontend/ React + Vite SPA deploy/ публикация и эксплуатация: vps/ (Caddy), pi/ (прод), tunnel/ и backup/ (образы) scripts/ build-push.* (сборка и пуш образов), fs-backup.* (бэкапы с ПК), export-*.sh Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI) docker-compose.yml прод на Pi: app + tunnel + backup docker-compose.test.yml тест-клон прода на ПК: app + tunnel + backup docker-compose.temp.yml временный прод на ПК вместо Pi: app + tunnel run.ps1 / run.sh единый лаунчер dev/test .env.example шаблон единого .env ``` ## Локальная разработка ### Единый лаунчер (`run.ps1` / `run.sh`) После разовой настройки (ниже) dev и test запускаются **одной командой** — что именно, решает `APP_ENV` в корневом `.env`: ```powershell .\run.ps1 # Windows (Linux / macOS / Git Bash: ./run.sh) ``` | `APP_ENV` в `.env` | что делает лаунчер | |---|---| | `development` | сначала `alembic upgrade head`, затем `uvicorn --reload` (бэк) + `vite` (фронт) нативно: `run.ps1` — в отдельных окнах, `run.sh` — в текущем терминале (Ctrl+C останавливает оба). При `LOCAL_PUBLIC=vps` дополнительно поднимает SSH-туннель на `forbidden-stars.ru` | | `test` | `docker compose -f docker-compose.test.yml up --build -d` — прод-клон (app + tunnel + backup); портов на хост нет, открывается на `https://forbidden-stars.ru` | | `production` | не запускает — прод деплоится отдельно (см. «Production» и «Git и деплой») | Разовая настройка перед первым запуском — поднять venv бэка и зависимости фронта (после неё повседневный цикл — просто `.\run.ps1`). Vite проксирует `/api` на бэкенд. ### 1) Бэкенд **Windows PowerShell** (команды по одной — в PowerShell 5.1 нет `&&`): ```powershell cd backend python -m venv .venv .\.venv\Scripts\Activate.ps1 # если ругается на политику: Set-ExecutionPolicy -Scope CurrentUser RemoteSigned pip install -e ".[dev]" # .[dev] — один аргумент (пакет + dev-зависимости) Copy-Item ..\.env.example ..\.env # ЕДИНЫЙ .env лежит в КОРНЕ репозитория alembic upgrade head # применит миграции и сидинг справочников python -m app.bootstrap # справочники + создаст/синхронизирует администратора из .env (опционально) uvicorn app.main:app --reload --timeout-graceful-shutdown 2 # http://localhost:8000 (Swagger: /api/docs) ``` **Windows cmd.exe** (здесь `&&` поддерживается): ```bat cd backend python -m venv .venv .venv\Scripts\activate.bat pip install -e ".[dev]" copy ..\.env.example ..\.env alembic upgrade head python -m app.bootstrap uvicorn app.main:app --reload --timeout-graceful-shutdown 2 ``` **Linux / macOS / Git Bash:** ```bash cd backend python -m venv .venv && . .venv/Scripts/activate # на *nix: . .venv/bin/activate pip install -e ".[dev]" cp ../.env.example ../.env alembic upgrade head python -m app.bootstrap uvicorn app.main:app --reload --timeout-graceful-shutdown 2 ``` > В dev тот же bootstrap (справочники + админ из `.env`) выполняется сам при старте uvicorn, > поэтому `python -m app.bootstrap` вручную обычно не нужен. Dev-БД, загрузки и ачивки > лежат в `backend/data/dev/` (пути в `.env` относительные — запускайте из `backend/`). > **`--timeout-graceful-shutdown` обязателен.** Открытая вкладка держит SSE-поток > `/api/events`, и без лимита `--reload` ждёт его закрытия вечно — сайт висит на загрузке, > а в логе только `Reloading...`. > **Единый `.env` — в корне репозитория** (`ForbidenStarsApp/.env`), рядом с `.env.example`. > Его читают и бэкенд (через абсолютный путь, независимо от рабочей папки), и `docker compose`. > Файл — локальный, на каждой машине свой (dev/prod различаются строкой `APP_ENV`). ### 2) Фронт (в отдельном терминале) ```bash cd frontend npm install npm run gen:api # сгенерирует типы из живого OpenAPI (бэкенд должен быть запущен) npm run dev # http://127.0.0.1:5173 или http://localhost:5173 (оба стека) ``` Вход в dev-режиме — экран `/login`: логин/пароль, Telegram и вход по нику без пароля (stub); в проде stub нет (см. раздел «Аутентификация»). ## Production (Docker на Pi) На Pi нужны только **два файла** — `docker-compose.yml` и `.env`: образы `app`, `tunnel` и `backup` собираются на ПК под arm64 и пушатся в Gitea-реестр, Pi тянет их сам. ```powershell # ПК (обычно с ветки main): собрать и опубликовать образы docker login gitea.arseniev.info .\scripts\build-push.ps1 ``` ```bash # Pi, папка с docker-compose.yml и .env docker compose up -d # pull_policy: always — тянет свежие образы, без сборки ``` Портов на хост нет — прод доступен только на `https://forbiddenstars.ru` через туннель-контейнер. FastAPI отдаёт собранный SPA и API с одного origin. Миграции, сидинг справочников и создание админа выполняются автоматически при старте (`entrypoint.sh`). В production закрыты OpenAPI/Swagger, а с дефолтным или коротким `SECRET_KEY` либо дефолтным `ADMIN_PASSWORD` приложение не стартует. Учтите: `ADMIN_PASSWORD` применяется только при первом создании админа, дальше его смена в `.env` ни на что не влияет (задача #73). Пошагово — [`deploy/pi/README.md`](deploy/pi/README.md), бэкапы — [`deploy/backup/README.md`](deploy/backup/README.md). ## Test — прод-клон в контейнере на ПК Тот же `Dockerfile` и поведение, что у прода (FastAPI отдаёт SPA, БД на томе, вход игроков по логину/паролю или через Telegram), но образ собирается локально — для проверки прод-сборки до выката на Pi. Портов на хост **нет**: тест-клон виден только на `https://forbidden-stars.ru` через свой туннель-контейнер (ключ — файл `deploy/tunnel/id_tunnel`). Проще всего — через лаунчер: поставить `APP_ENV=test` в `.env` и запустить `.\run.ps1`. Вручную (тот же эффект): ```bash docker compose -f docker-compose.test.yml up -d --build # открыть https://forbidden-stars.ru (Swagger: /api/docs) docker compose -f docker-compose.test.yml logs -f app docker compose -f docker-compose.test.yml down -v # остановить и стереть тестовые данные ``` - Читает **тот же `.env`**, что dev/prod (отдельного `.env.test` больше нет); внутри контейнера `APP_ENV` форсится в `test` (см. `docker-compose.test.yml`). Cookie — Secure (снаружи HTTPS). - От прода test отличается тем, что OpenAPI/Swagger открыт и **нет** fail-fast по дефолтным секретам, хотя контур публичный (задача #69). Не держите в `.env` дефолтные `SECRET_KEY` / `ADMIN_PASSWORD`, когда поднимаете test, — особенно после `restore-test` с прод-данными. - Данные — на отдельных томах `db-data-test` / `uploads-data-test` / `achievements-data-test` (и `backup-data-test` у контейнера бэкапов, он работает без расписания и без VPS); с dev и Pi не пересекаются. - Слот VPS 9001 общий с dev-туннелем (`LOCAL_PUBLIC=vps`) — поднимайте что-то одно. - Вход: **админ-панель** (`/admin/login`) работает сразу по логину/паролю; **игроки** — по логину/паролю сразу, через Telegram — при настроенном боте (`/setdomain` → `forbidden-stars.ru`). - Учебное восстановление прод-бэкапа в тест-клон — `.\scripts\fs-backup.ps1 restore-test` ([`deploy/backup/README.md`, шаг 7](deploy/backup/README.md#шаг-7-учебное-восстановление-на-тест-клоне)). - **Не используйте `$` в секретах.** Единый `.env` читают и pydantic (dev — `$` дословно), и docker compose (test/prod — `$` = подстановка переменной). Чтобы значение совпадало везде, в `SECRET_KEY`/`ADMIN_PASSWORD` не должно быть `$`. Удобно генерировать так: `python -c "import secrets;print(secrets.token_urlsafe(48))"` (даёт только `[A-Za-z0-9_-]`). ## Аутентификация Методы входа зависят от окружения (`APP_ENV`): | | dev | test / prod | |---|---|---| | Логин (= ник) и пароль | ✓ | ✓ (основной) | | Telegram Login Widget | ✓ | ✓ | | Вход по нику без пароля (stub) | ✓ | ✗ (физически отсутствует) | - **Логин и пароль** (`app/auth/password.py`) — основной вход. Логин — это ник игрока (смена ника меняет логин). Пароль: от 8 символов, не длиннее 72 байт, хранится bcrypt. От перебора — окно 15 минут в памяти процесса: 5 неудач на пару «IP + логин», 20 на IP и 50 на аккаунт (независимо от IP), дальше `429 TOO_MANY_ATTEMPTS`. Регистраций — не больше 10 с одного IP за окно. Игрок без пароля (из Telegram или созданный до паролей) после входа видит обязательное окно «Задайте пароль»: закрыть его нельзя, только задать пароль или выйти; в dev-сборке есть кнопка «Позже (dev)». В профиле пароль меняется (нужен текущий) и привязывается Telegram (ник не меняется). Забытый пароль задаёт админ на вкладке аккаунтов — почту приложение не хранит. Смена или сброс пароля завершает все прежние сессии игрока (при смене в профиле текущее устройство остаётся в системе); «Выйти» отзывает токен этого устройства. - **Stub-вход (по нику)** и **жёсткое удаление аккаунтов** в админке — только для разработки. Их код **физически не попадает в прод:** файлы `backend/app/auth/dev_stub.py`, `backend/app/routers/dev_auth.py` и `backend/app/routers/dev_admin.py` исключены из Docker-образа (`.dockerignore`), роутеры подключаются лишь при `APP_ENV=development` (`app/main.py`), а на фронте dev-блоки вырезаются из прод-сборки (`import.meta.env.DEV`). В test/prod аккаунт можно только отключить. - **Telegram:** сервер проверяет подпись виджета (HMAC по `TELEGRAM_BOT_TOKEN`) и свежесть данных (не старше суток). Первый вход регистрирует игрока под Telegram-тегом; если такой ник занят или некорректен, фронт просит выбрать другой. `GET /api/auth/config` отдаёт доступные методы и `telegram_bot_username` для виджета. **Настройка Telegram (когда будете подключать реальный вход):** 1. Создать бота у [@BotFather](https://t.me/BotFather) → получить **токен** и **username**. 2. `/setdomain` у BotFather → оба домена: `forbiddenstars.ru` (prod) и `forbidden-stars.ru` (dev/test). 3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`). 4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает. Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех окружениях. Страница — `/admin/login`; из интерфейса туда ведёт удержание кнопки «Меню» 10 секунд. ## Окружения (dev / test / prod) Один и тот же код; контур задаёт `APP_ENV` в **едином** `.env` (его читает лаунчер): | | dev | test (прод-клон локально) | prod (Pi) | |---|---|---|---| | Запуск | `.\run.ps1` → `uvicorn --reload` + `vite` | `.\run.ps1` → `docker compose -f docker-compose.test.yml` | `docker compose up -d` | | `APP_ENV` | `development` | `test` (форсится в compose) | `production` (форсится в compose) | | Env-файл | единый `.env` | единый `.env` | единый `.env` (на Pi) | | Раздача SPA | Vite (HMR), `:5173` | FastAPI, только через `https://forbidden-stars.ru` | FastAPI, только через `https://forbiddenstars.ru` | | База данных | `backend/data/dev/…` | том `db-data-test` (`/data`) | том `db-data` (`/data`) | | Вход игроков | пароль + Telegram + ник (stub) | пароль + Telegram | пароль + Telegram | | Swagger (`/api/docs`) | ✓ | ✓ | ✗ | | Fail-fast по дефолтным секретам | ✗ | ✗ | ✓ | - **Один `.env` на машину** в корне (рядом с `.env.example`). `APP_ENV` в нём решает, что запустит лаунчер (`development`/`test`); прод-контейнер это значение **игнорирует** и всегда `production`. Отдельного `.env.test` больше нет. - **Структура БД одна** (общие миграции Alembic), **файлы разные**: dev → `DEV_DATABASE_URL` (`backend/data/dev/`), test и prod → `PROD_DATABASE_URL` (том `/data`; у test и prod это РАЗНЫЕ тома). Так же раздельно лежат загрузки (`*_UPLOAD_DIR`) и ачивки (`*_ACHIEVEMENTS_DIR`). - **В Docker идёт только прод-код:** dev-вход (stub), dev-удаление аккаунтов и тесты физически исключены из образа (`.dockerignore`); `test` собирается из того же `Dockerfile`, что и прод, просто локально и с `APP_ENV=test`. - **Внимание:** данные дева (`backend/data/dev/`) сейчас **попадают** в образ — правило `data/` в `.dockerignore` исключает только корневую папку `data/` (задача #71). ## Git и деплой - Ветка **`dev`** — рабочая: весь код, лаунчер, тесты. Повседневная разработка и `test` здесь. - Ветка **`main`** — релиз прода: готовое промоутишь из `dev` через `merge dev→main`. Файлы во всех ветках одинаковы (окружение задаёт `.env`/compose, а не ветка) → merge безболезненный; чистоту прод-образа обеспечивает `.dockerignore`, а не разные наборы файлов. - **Деплой на Pi:** на ПК с ветки `main` — `.\scripts\build-push.ps1` (собирает и пушит образы app + tunnel + backup под arm64), на Pi — `docker compose up -d`. На Windows нужна именно PS-версия скрипта (`build-push.sh` из PowerShell уходит в WSL). - **Чистая выгрузка в папку без git** (опц., к деплою на Pi не относится): `scripts/export-prod.sh