Initial commit: site monorepo with API, web, and infra.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
влад
2026-07-16 10:11:54 +03:00
co-authored by Cursor
commit 016910ffb7
447 changed files with 73972 additions and 0 deletions
+41
View File
@@ -0,0 +1,41 @@
# Документация Compton
README в корне — только «как запустить за 30 секунд». Всё остальное — здесь.
Мы сознательно держим **мало файлов**, но каждый — **плотный**: таблицы, схемы, команды. Можно с лёгким юмором, но без простыней на 500 строк.
## Карта
| Документ | Когда открывать |
|----------|-----------------|
| [project.md](./project.md) | «Где что лежит?», архитектура, API, маршруты, env |
| [deploy.md](./deploy.md) | Запуск, **стандартные логины dev**, staging/prod, troubleshooting |
| [security.md](./security.md) | Auth, JWT revoke, секреты, nginx, чеклист prod |
| [release.md](./release.md) | Перед выкладкой: E2E, k6, ZAP, Lighthouse, smoke |
| [TZ.md](./TZ.md) | Полное ТЗ — источник правды по требованиям |
## Быстрые ссылки
```bash
# dev
python apps/api/scripts/bootstrap_install.py
docker compose --profile docker-web up -d --build
# тесты
pnpm --filter web test:ci
cd apps/api && python -m pytest --cov=app --cov-fail-under=90
# prod smoke (после деплоя)
./infra/scripts/smoke-prod.sh https://your-domain.com
```
## Что куда не кладём
| Не в git | Почему |
|----------|--------|
| `apps/api/data/secrets/install.env` | пароли БД, JWT, MinIO |
| `.env`, `apps/api/.env` | локальные секреты |
| `apps/api/data/logs/` | runtime-логи |
| `node_modules/`, `.venv/` | очевидно |
Если секрет утёк в git — считайте его скомпрометированным. Force-push не спасает совесть.
+1510
View File
File diff suppressed because it is too large Load Diff
+44
View File
@@ -0,0 +1,44 @@
# WESP Orchestrator — production deploy
## Bootstrap
1. Copy env template and run install secrets (`site/scripts/` or admin UI).
2. Start stack: `docker compose -f site/infra/docker/docker-compose.prod.yml up -d`.
3. Run migrations: `docker compose exec api alembic upgrade head`.
4. Sync WESP static UI: `site/scripts/sync-wesp-ui.sh`.
## Hub pairing
1. Open Enterprise cabinet → **Generate pairing code**.
2. On farm hub (K-hub server role): Admin → Orchestrator sync → enter VPS URL + code.
3. Verify heartbeat and sync metrics in cabinet.
## Worker
- Default loop: `python -m app.worker` (reconcile every 5 min).
- Celery (recommended prod): `celery -A app.celery_app worker --loglevel=info` (service `celery-worker` in compose).
## Hub (WESP)
1. Run migration: `flask db upgrade` (revision `0025` / `web_user_lab_access` → orchestrator tables).
2. Configure orchestrator sync in hub admin (URL + pairing code).
3. Catalog edits enqueue `orchestrator_outbox`; reports enqueue `report_outbox`.
## nginx
- Zootech static: `/recipes`, `/components`, … → `public/wesp/*.html` (see `infra/nginx/default.tls.conf`).
- API: `/api/` → FastAPI.
## Security checklist
- Rotate `JWT_ACCESS_SECRET`, `JWT_REFRESH_PEPPER`, hub API keys.
- Enable PostgreSQL RLS in production (`DATABASE_URL=postgresql+psycopg://...`).
- Review `site/docs/security.md` before go-live.
## Tests (CI gate)
```bash
cd site/apps/api && .venv/bin/python -m pytest tests/modules/sync/test_orchestrator_dual_hub_e2e.py -q
```
Dual-hub E2E must pass before merge.
+196
View File
@@ -0,0 +1,196 @@
# Деплой и эксплуатация
От «запустил на ноуте» до «живёт на VPS и не стыдно показать security.md».
## Среды
```mermaid
flowchart LR
Dev[docker-compose.yml] --> St[staging]
St --> QA[k6 + ZAP + E2E]
QA --> Prod[production]
```
| Среда | Compose | APP_ENV | Docs | Demo users |
|-------|---------|---------|------|------------|
| Dev | `docker-compose.yml` | development | ✅ | ✅ |
| CI/E2E | `docker-compose.test.yml` | test | ✅ | ✅ |
| Staging | `infra/docker/docker-compose.staging.yml` | staging | ❌ | ❌ |
| Production | `infra/docker/docker-compose.prod.yml` | production | ❌ | ❌ |
## Локальная разработка
```bash
# 1. venv + зависимости backend (один раз)
python3 -m venv .venv
source .venv/bin/activate # Windows: .venv\Scripts\activate
pip install -r apps/api/requirements-dev.txt
# 2. Секреты установки (один раз, до первого docker compose up)
python apps/api/scripts/bootstrap_install.py
# 3. Полный стек в Docker (API + БД + web)
docker compose --profile docker-web up -d --build
# 4. Проверка
curl http://localhost:8000/api/v1/health
```
| Сервис | URL |
|--------|-----|
| Web | http://localhost:5173 |
| API | http://localhost:8000 |
| PG/Redis/MinIO на хост | `docker-compose.dev-ports.yml` → 5432, 6379, 9000/9001 |
> Bootstrap и локальные тесты (`pytest`, `mypy`) — через активированный `.venv`. Docker API использует свой образ; venv нужен для скриптов и разработки на хосте.
**Гибрид** (инфра в Docker, frontend локально):
```bash
source .venv/bin/activate
docker compose up -d
pnpm install
pnpm --filter web dev
```
**Install secrets на хост** (DBeaver): Admin → Security → Install Secrets → Reveal DB password.
### Стандартные логины (dev)
Создаются при seed на старте API. Пароли по умолчанию — из `apps/api/.env.example` (или дефолты в `config.py`).
| Email | Пароль | Env | Роль | Superuser | Куда заходит |
|-------|--------|-----|------|:---------:|--------------|
| `admin@compton.example` | `Admin1234` | `ADMIN_INITIAL_PASSWORD` | admin | да | `/admin` — Users, Content, Security, Diagnostics, secrets |
| `ops@compton.example` | `OpsAdmin1234` | `DEMO_OPS_PASSWORD` | admin | нет | `/admin` — Users, Content, Activity (без Security) |
| `user@compton.example` | `User1234` | `DEMO_USER_PASSWORD` | user | — | `/profile` |
**CMS-страницы (seed):** `about`, `privacy`, `terms``/pages/about` и т.д.
> **Staging/production:** `SEED_DEMO_USERS=false` — demo `user@` и `ops@` **не создаются**, только admin + CMS. Пароль admin задаётся через `ADMIN_INITIAL_PASSWORD` **до первого seed**, потом — сменить в UI.
## Staging
**Нужно:** VPS, DNS, TLS certs в `infra/docker/certs/`, SMTP.
```bash
git clone https://git.groupkomton.ru/Matvey/site.git && cd site
python3 apps/api/scripts/bootstrap_install.py
cp infra/docker/.env.staging.example infra/docker/.env.staging
# правим: домен, CORS, SMTP, ADMIN_INITIAL_PASSWORD
./infra/docker/deploy-staging.sh
```
Проверка:
```bash
curl -fsS https://STAGING/api/v1/health
curl -fsS -o /dev/null -w "%{http_code}" https://STAGING/api/v1/docs # 404
./infra/scripts/health-check.sh https://STAGING
```
Nginx: `default.tls.conf` — 80→443, HSTS.
## Production
```bash
python3 apps/api/scripts/bootstrap_install.py
cp infra/docker/.env.production.example infra/docker/.env.production
./infra/docker/deploy-prod.sh infra/docker/.env.production
```
**Сразу после bootstrap:**
1. Бэкап `install.env` off-server (зашифровать)
2. Сменить пароль admin
| Сервис | Host ports |
|--------|------------|
| nginx | 80, 443 |
| api, web, pg, redis, minio | internal only |
```bash
./infra/scripts/smoke-prod.sh https://YOUR_DOMAIN
```
### Rollback
```bash
docker compose -f infra/docker/docker-compose.prod.yml down
git checkout PREVIOUS_TAG
./infra/docker/deploy-prod.sh infra/docker/.env.production
```
## Бэкапы и мониторинг
| Что | Команда / как |
|-----|---------------|
| PostgreSQL | `./infra/scripts/backup-postgres.sh``backups/postgres-*.sql.gz` |
| install.env | `cp …/install.env backups/install.env.$(date +%F).enc` + gpg |
| Uptime | `./infra/scripts/health-check.sh URL` или UptimeRobot на `/api/v1/health` + `/` |
| Логи | logrotate для `server.log`, `admin-audit.jsonl` |
## Восстановление секретов
`install.env` — единственный источник runtime-секретов. Потеряли — не генерируйте новый вслепую.
### Симптомы
- `password authentication failed for user "compton_app"`
- bootstrap после того, как Postgres volume уже создан
### Fix (есть бэкап)
```bash
docker compose down
# восстановить apps/api/data/secrets/install.env
docker compose up -d --build
```
### Fix (нет бэкапа)
| Вариант | Данные |
|---------|--------|
| Reveal из другой среды | сохраняются |
| `down -v` + bootstrap (**только dev**) | **удаляются все** |
```bash
docker compose --profile docker-web down -v
python apps/api/scripts/bootstrap_install.py
docker compose --profile docker-web up -d --build
```
## SMTP
Staging/prod: `EMAIL_DELIVERY_MODE=smtp`. Dev: `memory` (письма в RAM, SMTP не нужен).
| Env | Назначение |
|-----|------------|
| `SMTP_HOST`, `SMTP_PORT`, `SMTP_FROM` | сервер |
| `FRONTEND_URL` | ссылки в письмах |
## Troubleshooting
| Проблема | Решение |
|----------|---------|
| `install.env missing` | `python apps/api/scripts/bootstrap_install.py` |
| `password authentication failed` | [восстановление секретов](#восстановление-секретов) |
| Login failed | `curl …/health`, проверить `.env`, restart web |
| 401 refresh в консоли (гость) | норма на публичных страницах |
| Logout после F5 на `/admin` | rebuild web, перелогиниться |
| «На сайт» ведёт на login | должно быть `href="/"`, rebuild |
| Port 5173 busy | stop Docker web **или** local Vite |
| CORS | `VITE_USE_API_PROXY=true`, не бить напрямую :8000 |
| Нет Security/Diagnostics | логин `admin@`, не `ops@` |
| Docker web: missing modules | `docker compose … up -d --build web` |
## Compose-справочник
| Файл | Назначение |
|------|------------|
| `docker-compose.yml` | dev |
| `docker-compose.dev-ports.yml` | порты на хост |
| `docker-compose.test.yml` | CI/E2E |
| `infra/docker/docker-compose.staging.yml` | staging |
| `infra/docker/docker-compose.prod.yml` | production |
Deploy: `infra/docker/deploy-staging.sh`, `deploy-prod.sh`.
+220
View File
@@ -0,0 +1,220 @@
# Структура и архитектура
Monorepo-lite: статический лендинг + React SPA + FastAPI + PostgreSQL + Redis + MinIO.
Если вы искали микросервисы на Kubernetes — это другой коридор.
## Стек
| Слой | Технологии |
|------|------------|
| Frontend | React 19, TS, Vite, React Router, TanStack Query, Zustand, RHF+Zod, Ant Design |
| Backend | FastAPI, SQLAlchemy 2, Alembic, Pydantic v2 |
| Данные | PostgreSQL 16, Redis 7, MinIO |
| Инфра | Docker Compose, Nginx, GitHub Actions |
| Качество | Vitest, Playwright, pytest (≥90% / ≥85% cov) |
## Дерево репозитория
```
site/
├── apps/
│ ├── api/ # Backend
│ │ ├── app/
│ │ │ ├── core/ # crypto, jwt_denylist, redis, install_secrets…
│ │ │ ├── db/ # models, seed, migrations helpers
│ │ │ └── modules/ # auth, users, content, admin, media, test
│ │ ├── migrations/ # Alembic
│ │ ├── scripts/ # bootstrap_install.py, docker_entrypoint.py
│ │ ├── tests/
│ │ └── data/
│ │ ├── secrets/ # install.env (gitignore!)
│ │ └── logs/ # server.log, admin-audit.jsonl (gitignore)
│ └── web/
│ ├── index.html # лендинг /
│ ├── app.html # SPA entry
│ ├── main/ # статика лендинга (CSS/JS/video)
│ ├── src/
│ │ ├── app/ # router, guards
│ │ ├── modules/ # auth, profile, admin, content, landing
│ │ ├── pages/
│ │ └── shared/ # api client, ui
│ └── e2e/ # Playwright
├── packages/ # eslint-config, shared-types (target)
├── infra/
│ ├── docker/ # staging/prod compose, deploy.sh
│ ├── nginx/ # default.conf, default.tls.conf
│ ├── k6/ # load test §17.2
│ └── scripts/ # backup, health, smoke
├── docs/ # вы здесь
├── docker-compose.yml # dev
└── docker-compose.test.yml # CI / E2E
```
## Runtime
```mermaid
flowchart TB
subgraph browser [Браузер]
L[index.html /]
S[app.html SPA]
end
subgraph edge [Nginx :80/:443]
N[TLS + headers]
end
subgraph internal [Docker internal]
W[web]
A[api]
PG[(PostgreSQL)]
R[(Redis)]
M[(MinIO)]
end
L --> N
S --> N
N --> W
N --> A
A --> PG
A --> R
A --> M
```
**Prod/staging:** наружу только nginx. Postgres, Redis, MinIO — без host-портов.
## Frontend
### Два входа (dual-entry)
| Entry | URL | Содержимое |
|-------|-----|------------|
| `index.html` | `/` | Маркетинговый лендинг (`main/`) |
| `app.html` | `/login`, `/admin`, … | React SPA |
Vite переписывает SPA-пути на `app.html` (`vite.main-static.ts`).
### Маршруты
| Путь | Guard | Кто |
|------|-------|-----|
| `/` | — | все |
| `/login`, `/register`, `/forgot-password`, `/reset-password` | GuestGuard | гости |
| `/verify`, `/pages/:slug` | — | публично |
| `/profile` | AuthGuard | user |
| `/admin` | AdminGuard | admin |
**Auth UX:**
- Access JWT — только в памяти (Zustand), не localStorage
- Refresh — HttpOnly cookie, `Path=/api/v1/auth`
- После login: admin → `/admin`, user → `/profile`
- Кнопка «На сайт» в админке — **полный** переход на `/` (не React Router)
### Админка (WESP-style)
| Раздел | Кому | Что |
|--------|------|-----|
| Users | admin | CRUD пользователей |
| Content | admin | CMS |
| Security | superuser | runtime settings, Install Secrets |
| Diagnostics | superuser | health checks |
| Activity | admin | audit feed, server log |
Тема Light/Dark — `localStorage.wespAdminTheme`. Auth-страницы — zootech-карточки (`#48816d`).
## Backend API
База: `/api/v1`
| Модуль | Эндпоинты (основное) |
|--------|----------------------|
| health | `GET /health` |
| auth | register, login, logout, refresh, verify, forgot/reset password |
| users | `GET/PATCH /me`, password, avatar |
| content | публичные pages + admin CRUD |
| admin | users, stats, settings, diagnostics, secrets, activity |
| media | подписанные URL файлов |
| test | `/test/emails/latest-token`**только** E2E |
### Core (`apps/api/app/core/`)
| Модуль | Зачем |
|--------|-------|
| `crypto.py` | bcrypt, JWT, HMAC, token hash — одна точка |
| `jwt_denylist.py` | мгновенный revoke access JWT |
| `install_secrets.py` | bootstrap + lock |
| `dependencies.py` | `get_current_user` |
| `redis.py` | rate limit + JWT revoke |
| `storage.py` | MinIO / memory |
## База данных
```mermaid
erDiagram
users ||--o| user_profiles : has
users ||--o{ refresh_tokens : owns
users ||--o{ password_reset_tokens : owns
users ||--o{ email_verification_tokens : owns
```
Таблицы: `users`, `user_profiles`, `refresh_tokens`, `password_reset_tokens`, `email_verification_tokens`, `content_pages`.
```bash
cd apps/api && alembic upgrade head
```
CHECK constraints на `role`, `status`; superuser только при `role=admin`. Cleanup expired tokens при старте API.
### Seed: стандартные логины (dev)
| Email | Пароль | Env |
|-------|--------|-----|
| admin@compton.example | Admin1234 | `ADMIN_INITIAL_PASSWORD` |
| ops@compton.example | OpsAdmin1234 | `DEMO_OPS_PASSWORD` |
| user@compton.example | User1234 | `DEMO_USER_PASSWORD` |
CMS: `about`, `privacy`, `terms`. Подробнее — [deploy.md § логины](./deploy.md#стандартные-логины-dev).
## Переменные окружения (ключевые)
| Переменная | Где | Назначение |
|------------|-----|------------|
| `DATABASE_URL` | api | PostgreSQL |
| `APP_ENV` | api | development / staging / production |
| `JWT_ACCESS_SECRET`, `JWT_REFRESH_PEPPER` | api | токены |
| `REDIS_URL` | api | rate limit + JWT revoke (prod обязателен) |
| `SEED_DEMO_USERS` | api | `false` на staging/prod |
| `ADMIN_INITIAL_PASSWORD` | api | пароль admin при seed (default `Admin1234`) |
| `DEMO_USER_PASSWORD`, `DEMO_OPS_PASSWORD` | api | demo user/ops (только dev) |
| `ENABLE_DOCS` | api | `false` на prod |
| `ENABLE_TEST_ROUTES` | api | `true` только E2E |
| `EMAIL_DELIVERY_MODE` | api | `memory` (dev) / `smtp` (prod) |
| `STORAGE_MODE` | api | `s3` / `memory` |
| `VITE_API_URL` | web | `http://api:8000` в Docker |
| `VITE_USE_API_PROXY` | web | `true` в dev |
Полные примеры: `apps/api/.env.example`, `apps/api/.env.production.example`.
### Runtime settings
`apps/api/data/compton_settings.json` — toggles без секретов. Superuser: `GET/PATCH /admin/settings`. Env с тем же ключом = **lock** (нельзя менять из UI).
## Docker Compose
| Файл | Когда |
|------|-------|
| `docker-compose.yml` | локальная разработка |
| `docker-compose.dev-ports.yml` | PG/Redis/MinIO на хост (DBeaver) |
| `docker-compose.test.yml` | CI, Playwright |
| `infra/docker/docker-compose.staging.yml` | staging VPS |
| `infra/docker/docker-compose.prod.yml` | production VPS |
## CI
`.github/workflows/ci.yml`: lint → types → mypy → tests → audit → Bandit → gitleaks → E2E.
## Тесты
```bash
pnpm --filter web test:ci
cd apps/api && python -m pytest --cov=app --cov-fail-under=90
pnpm --filter web e2e
```
E2E поднимает API `:8001` + Vite `:5175`. Против staging: `E2E_BASE_URL=… E2E_START_API=false`.
+113
View File
@@ -0,0 +1,113 @@
# Релиз и QA gates
Перед production не «авось прокатит», а чеклист из ТЗ §17. Если что-то красное — сначала staging, потом prod. Живёт один раз.
## Pipeline
```mermaid
flowchart TD
CI[CI green] --> ST[Staging + TLS]
ST --> E2E[E2E 11/11]
ST --> K6[k6 pass]
ST --> ZAP[ZAP 0 High/Crit]
ST --> LH[Lighthouse ≥ 85]
E2E --> PROD[Production deploy]
K6 --> PROD
ZAP --> PROD
LH --> PROD
PROD --> SM[smoke-prod.sh]
SM --> DNS[DNS cutover]
DNS --> MON[24h мониторинг]
```
## Регрессия E2E
**Последний локальный прогон:** 2026-07-14 — backend 137 / 90.43% cov, frontend 46.
### Staging
```bash
E2E_BASE_URL=https://staging.example.com E2E_START_API=false pnpm --filter web e2e
```
| # | Сценарий | Local | Staging |
|---|----------|:-----:|:-------:|
| 1 | Landing hero, reduced-motion | ☐ | ☐ |
| 2 | Register → verify → login → profile → logout | ☐ | ☐ |
| 3 | Forgot → reset → login | ☐ | ☐ |
| 4 | Admin publish → public slug | ☐ | ☐ |
| 5 | Block user → login denied | ☐ | ☐ |
| 6 | Pending → нет `/profile` | ☐ | ☐ |
| 7 | Refresh rotation | ☐ | ☐ |
| 8 | IDOR user A ≠ user B | ☐ | ☐ |
| 9 | Admin не блокирует себя / last admin | ☐ | ☐ |
| 10 | Avatar: bad MIME / size / SVG | ☐ | ☐ |
| 11 | Block → access JWT 401 TOKEN_REVOKED | ☐ | ☐ |
### Локально
```bash
pnpm --filter web e2e # API :8001, Vite :5175
```
## k6 (§17.2)
```bash
k6 run infra/k6/mvp-load-test.js -e BASE_URL=https://staging.example.com
```
| Параметр | Порог |
|----------|-------|
| VU / ramp | 50 / 5 min |
| Mix | 35% list, 25% login, 20% me, 10% refresh, 10% slug |
| p95 | < 300 ms |
| Errors | < 1% |
## OWASP ZAP
```bash
docker run --rm -v "$(pwd):/zap/wrk:rw" -t ghcr.io/zap/zaproxy:stable \
zap-baseline.py -t https://staging.example.com -r zap-report.html
```
| Severity | Pass |
|----------|------|
| High, Critical | **0** |
Medium/Low — review руками. `zap-report.html` — в архив релиза.
## Lighthouse
```bash
npx lighthouse https://staging.example.com \
--preset=mobile --only-categories=performance \
--output=json --output-path=./lighthouse-report.json
```
| Метрика | ≥ |
|---------|---|
| Performance (mobile, `/`) | 85 |
Не прошло — hero video `preload="none"`, font swap, меньше JS на лендинге.
## Release gates (сводка)
- [ ] CI green на `main`
- [ ] E2E 11/11 на staging
- [ ] k6 pass
- [ ] ZAP 0 High/Critical
- [ ] Lighthouse ≥ 85
- [ ] [security.md](./security.md) staging-пункты
- [ ] `./infra/scripts/smoke-prod.sh` на prod
- [ ] DNS cutover + rollback plan (previous tag)
## CI локально
```bash
pnpm lint && pnpm typecheck
cd apps/api && python -m mypy app
pnpm --filter web test:ci
cd apps/api && python -m pytest --cov=app --cov-fail-under=90
```
Полный pipeline: `.github/workflows/ci.yml`.
+173
View File
@@ -0,0 +1,173 @@
# Безопасность
Compton MVP — не банк, но и не «admin/admin в prod». Ниже — как устроена защита и что проверить перед выкладкой.
## Auth: схема
```mermaid
sequenceDiagram
participant B as Браузер
participant API as FastAPI
participant RD as Redis
participant PG as PostgreSQL
B->>API: POST /auth/login
API->>PG: bcrypt verify
API->>RD: read auth_epoch
API-->>B: access JWT (memory) + refresh cookie
B->>API: GET /users/me + Bearer
API->>RD: jti denied? epoch ok?
alt revoked
API-->>B: 401 TOKEN_REVOKED
else ok
API-->>B: 200
end
B->>API: POST /auth/logout
API->>RD: deny_jti + revoke refresh
API-->>B: cookie cleared
```
## Токены
| Токен | Где живёт | Отзыв |
|-------|-----------|-------|
| Access JWT | память frontend | jti denylist + auth_epoch (Redis) |
| Refresh | HttpOnly cookie | rotation + family reuse detection |
| Email/reset | opaque → hash в БД | one-time, TTL 1ч |
JWT claims: `sub`, `role`, `jti`, `auth_epoch`, `exp`.
### Мгновенный revoke
`apps/api/app/core/jwt_denylist.py`:
| Redis key | Смысл |
|-----------|-------|
| `jwt:deny:{jti}` | конкретный access-токен |
| `auth:epoch:{user_id}` | версия сессий пользователя |
| Событие | Действие |
|---------|----------|
| Logout | deny jti + revoke refresh |
| Block | INCR epoch + revoke refresh |
| Смена/reset пароля | INCR epoch + revoke refresh |
**Production:** без Redis API не стартует. Redis упал — fail-closed (401, не «ну ладно»).
**Dev:** in-memory fallback (не путать с prod).
## Криптография
Всё через `apps/api/app/core/crypto.py`:
| Данные | Метод |
|--------|-------|
| Пароли | bcrypt cost 12 |
| Access JWT | HS256 |
| Refresh/email tokens | SHA-256 + pepper |
| Media URLs | HMAC-SHA256 + TTL |
| Install secrets | `secrets.token_*`, generate-once + lock |
## Install secrets
```bash
python apps/api/scripts/bootstrap_install.py # до первого docker compose up
```
| Файл | Содержимое |
|------|------------|
| `data/secrets/install.env` | PG, JWT, S3, MinIO |
| `install.meta.json` | install ID, lock time |
**Не ротировать** `POSTGRES_PASSWORD` / JWT после bootstrap без плана — иначе Postgres скажет фразу, которую вы уже видели, и будет прав.
Reveal: Admin → Security → Install Secrets (superuser, аудит в `admin-audit.jsonl`).
Восстановление: [deploy.md § восстановление](./deploy.md#восстановление-секретов).
## RBAC
| Правило | Enforcement |
|---------|-------------|
| `/admin` → role=admin | Guard + API |
| Security/Diagnostics/secrets → superuser | API + UI tabs |
| Нельзя block/demote себя | admin service |
| Last admin protected | admin service |
| blocked/pending → 403 refresh | auth service |
| forgot_password skip для blocked | auth service |
| IDOR на профиль | users router |
## HTTP / инфра
### Nginx headers
| Header | Значение |
|--------|----------|
| X-Frame-Options | DENY |
| X-Content-Type-Options | nosniff |
| Referrer-Policy | strict-origin-when-cross-origin |
| HSTS | `default.tls.conf` (staging/prod) |
### Прочее
- Origin/Referer на cookie-auth endpoints
- Rate limit (prod: обязателен)
- CMS: bleach, протоколы http/https/mailto
- Avatar: jpeg/png/webp, re-encode, **SVG — нет**
- CI: Bandit, pip-audit, gitleaks, npm audit
- Postgres/Redis/MinIO — internal network
- Firewall VPS: 22, 80, 443
## Production guards
`APP_ENV=production` → API **не стартует**, если:
| Проблема | Env |
|----------|-----|
| Test routes | `ENABLE_TEST_ROUTES=true` |
| OpenAPI | `ENABLE_DOCS=true` |
| Rate limit off | `ENABLE_RATE_LIMIT=false` |
| Cookie без Secure | `COOKIE_SECURE=false` |
| Placeholder JWT | `change-me-*` |
| Дефолтная БД | user:pass |
| Без SSL mode | нет `sslmode=require` |
| Без Redis | JWT revocation |
## Prod env (минимум)
| Переменная | Значение |
|------------|----------|
| `APP_ENV` | production |
| `ENABLE_DOCS` | false |
| `ENABLE_TEST_ROUTES` | false |
| `COOKIE_SECURE` | true |
| `ENABLE_RATE_LIMIT` | true |
| `EMAIL_DELIVERY_MODE` | smtp |
| `SEED_DEMO_USERS` | false |
| `DATABASE_URL` | …?sslmode=require |
## Чеклист
### Сделано в коде
- [x] JWT в memory, refresh HttpOnly
- [x] Anti-enumeration auth
- [x] Origin/Referer validation
- [x] RBAC + superuser
- [x] CMS sanitization
- [x] Nginx security headers
- [x] CI security scans
- [x] Install secrets bootstrap + lock
- [x] JWT jti denylist + auth_epoch
- [x] Admin audit log
### Проверить на staging
- [ ] `/api/v1/docs` → 404
- [ ] HSTS за TLS
- [ ] Dry-run recovery секретов
- [ ] ZAP: 0 High/Critical → [release.md](./release.md)
- [ ] k6 pass → [release.md](./release.md)
- [ ] Lighthouse ≥ 85 на `/`