Initial commit: site monorepo with API, web, and infra.
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -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
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -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
@@ -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
@@ -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`.
|
||||
@@ -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 на `/`
|
||||
Reference in New Issue
Block a user