Files
ForbiddenStarsApp/README.md
T

266 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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).