266 lines
20 KiB
Markdown
266 lines
20 KiB
Markdown
# 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-prod.sh
|
||
Dockerfile multi-stage сборка (фронт собирается node, отдаётся FastAPI)
|
||
docker-compose.yml прод на Pi: app + tunnel + backup
|
||
docker-compose.temp.yml временный прод на ПК вместо Pi: app + tunnel
|
||
run.ps1 / run.sh единый лаунчер dev
|
||
.env.example шаблон единого .env
|
||
```
|
||
|
||
## Локальная разработка
|
||
|
||
Всё проверяется в `development` — отдельного тестового контейнера нет.
|
||
|
||
### Единый лаунчер (`run.ps1` / `run.sh`)
|
||
|
||
После разовой настройки (ниже) dev запускается **одной командой** (лаунчер читает
|
||
`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` |
|
||
| `production` | не запускает — прод деплоится отдельно (см. «Production» и «Git и деплой») |
|
||
| другое значение | отказ: допустимы только `development` и `production` (бэкенд тоже не стартует) |
|
||
|
||
Разовая настройка перед первым запуском — поднять 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).
|
||
|
||
- **Не используйте `$` в секретах.** Единый `.env` читают и pydantic (dev — `$` дословно),
|
||
и docker compose (prod — `$` = подстановка переменной). Чтобы значение совпадало
|
||
везде, в `SECRET_KEY`/`ADMIN_PASSWORD` не должно быть `$`. Удобно генерировать так:
|
||
`python -c "import secrets;print(secrets.token_urlsafe(48))"` (даёт только `[A-Za-z0-9_-]`).
|
||
- **Временный прод на ПК** (вместо Pi): `docker compose -f docker-compose.temp.yml up -d --build` —
|
||
поведение production, локальная сборка x86, ключ туннеля из файла `deploy/tunnel/id_tunnel`,
|
||
тот же слот VPS 9000, что у Pi (одновременно не запускать).
|
||
|
||
## Аутентификация
|
||
|
||
Методы входа зависят от окружения (`APP_ENV`):
|
||
|
||
| | dev | 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`).
|
||
В проде аккаунт можно только отключить.
|
||
- **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).
|
||
3. В `.env`: `TELEGRAM_BOT_TOKEN=...`, `TELEGRAM_BOT_USERNAME=...` (без `@`).
|
||
4. Виджету нужен HTTPS-домен (см. «Домен и публикация») — по голому HTTP/localhost он не работает.
|
||
|
||
Админ-вход (секретная панель, логин+пароль) — отдельный механизм, доступен во всех
|
||
окружениях. Страница — `/admin/login`; из интерфейса туда ведёт удержание кнопки «Меню» 10 секунд.
|
||
|
||
## Окружения (dev / prod)
|
||
|
||
Один и тот же код; контур задаёт `APP_ENV` в **едином** `.env`. Допустимы только
|
||
`development` и `production` — с любым другим значением бэкенд не стартует:
|
||
|
||
| | dev | prod (Pi) |
|
||
|---|---|---|
|
||
| Запуск | `.\run.ps1` → `uvicorn --reload` + `vite` | `docker compose up -d` |
|
||
| `APP_ENV` | `development` | `production` (форсится в compose) |
|
||
| Env-файл | единый `.env` | единый `.env` (на Pi) |
|
||
| Раздача SPA | Vite (HMR), `:5173` | FastAPI, только через `https://forbiddenstars.ru` |
|
||
| База данных | `backend/data/dev/…` | том `db-data` (`/data`) |
|
||
| Вход игроков | пароль + Telegram + ник (stub) | пароль + Telegram |
|
||
| Swagger (`/api/docs`) | ✓ | ✗ |
|
||
| Fail-fast по дефолтным секретам | ✗ | ✓ |
|
||
|
||
- **Один `.env` на машину** в корне (рядом с `.env.example`). Прод-контейнер значение
|
||
`APP_ENV` из него **игнорирует** и всегда `production`.
|
||
- **Структура БД одна** (общие миграции Alembic), **файлы разные**: dev → `DEV_DATABASE_URL`
|
||
(`backend/data/dev/`), prod → `PROD_DATABASE_URL` (том `/data`). Так же раздельно лежат
|
||
загрузки (`*_UPLOAD_DIR`) и ачивки (`*_ACHIEVEMENTS_DIR`).
|
||
- **В Docker идёт только прод-код:** dev-вход (stub), dev-удаление аккаунтов и тесты физически
|
||
исключены из образа (`.dockerignore`).
|
||
- **Внимание:** данные дева (`backend/data/dev/`) сейчас **попадают** в образ — правило
|
||
`data/` в `.dockerignore` исключает только корневую папку `data/` (задача #71).
|
||
|
||
## Git и деплой
|
||
|
||
- Ветка **`dev`** — рабочая: весь код, лаунчер, тесты. Повседневная разработка и проверка здесь.
|
||
- Ветка **`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 <dir> [ref]` — через `git archive` + `export-ignore` из
|
||
`.gitattributes` (без тестов, stub-входа, `pyproject.toml`, README-файлов и лаунчера).
|
||
`dev_admin.py` в `export-ignore` пока не внесён (задача #70).
|
||
|
||
Секреты (`.env`) и данные (`data/`, `*.db`) в git не идут — см. `.gitignore`.
|
||
|
||
## Домен и публикация
|
||
|
||
Публичные адреса отдаёт **VPS-привратник** (Caddy + HTTPS твоими сертификатами), а
|
||
приложение само открывает к нему SSH reverse-туннель (дома белого IP нет — CGNAT).
|
||
У **прода** туннель — **отдельный контейнер** в `docker-compose.yml`, и портов на хост он
|
||
не публикует (доступен только через домен):
|
||
|
||
| | домен | как выставляется | слот VPS |
|
||
|---|---|---|---|
|
||
| prod | `forbiddenstars.ru` | туннель-контейнер (постоянно) → `app:8000` | 9000 |
|
||
| dev | `forbidden-stars.ru` | лаунчер (`run.ps1`/`run.sh`) при `LOCAL_PUBLIC=vps` → `localhost:5173` | 9001 |
|
||
|
||
- Прод (9000) и dev (9001) на **разных слотах/доменах** → работают одновременно. Временный
|
||
прод на ПК (`docker-compose.temp.yml`) занимает слот 9000 — одновременно с продом на Pi не запускать.
|
||
- Dev по умолчанию только на localhost; `LOCAL_PUBLIC=vps` + лаунчер выставляет его на домен.
|
||
В dev при этом открыты stub-вход по нику и Swagger — держите туннель поднятым только на
|
||
время проверки (задача #69).
|
||
- `COOKIE_SECURE` выводится автоматически (HTTPS-домен ⇒ Secure-cookie; dev+localhost ⇒ нет).
|
||
- Ключ туннеля: временный прод берёт файл `deploy/tunnel/id_tunnel`, прод на Pi —
|
||
`TUNNEL_KEY_B64` (base64) в `.env`, dev-туннель — системный `ssh` с ключом по умолчанию
|
||
(`~/.ssh`). Публичные части — в `authorized_keys` пользователя `tunnel` на VPS.
|
||
- Пошаговая настройка — в [`deploy/`](deploy/README.md): `vps/` (Caddy, сертификаты, юзер `tunnel`),
|
||
`pi/` (app + туннель + бэкапы в Docker), образ туннеля — `deploy/tunnel/`.
|
||
|
||
## Бэкапы
|
||
|
||
Контейнер `backup` (restic) в `docker-compose.yml` каждую ночь делает зашифрованный снимок
|
||
БД, `uploads` и `achievements` — на Pi (том `backup-data`) и на VPS по SFTP. С ПК снимки
|
||
скачиваются со сверкой sha256 (`scripts/fs-backup.ps1 pull`).
|
||
Настройка, восстановление и действия при гибели Pi — [`deploy/backup/README.md`](deploy/backup/README.md).
|
||
|
||
## Дополнения и фракции
|
||
|
||
| Дополнение | Фракции |
|
||
|---|---|
|
||
| База (всем) | Орки, Ультрамарины, Эльдары, Хаоситы |
|
||
| Forgotten Worlds | Имперская гвардия, Тау, Некроны, Тираниды |
|
||
| Forsaken Voids | Инквизиция, Сёстры битвы, Друкхари, Адептус Механикус |
|
||
|
||
Группа отмечает имеющиеся дополнения; при создании партии доступны только фракции
|
||
этих дополнений (база — всегда). Справочник сидится из `backend/app/seed/reference_data.py`.
|
||
Админ может переименовать фракцию в панели, но сейчас переименование откатывается при
|
||
каждом перезапуске приложения (задача #72).
|