docs: детализация дизайна Фазы 1B (persistence + REST API)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-07-03 13:25:49 +07:00
parent c0c8e3188d
commit 3d6e3110b3
@@ -154,3 +154,82 @@ users
- Экран добавления provider-учётки Selectel содержит: поле API-ключа, краткую инструкцию
и ссылку на страницу получения ключа в панели Selectel.
## Фаза 1B — детализация (persistence + REST API)
Строится поверх готового ядра 1A (`internal/model`, `internal/diff`, `internal/provider`,
`internal/provider/selectel`).
### Стек
- Доступ к БД: **pgx/v5** (`pgxpool`) + **sqlc** (`sql_package: pgx/v5`, `emit_json_tags`).
- Миграции: **goose** (`embed.FS`), применяются при старте через pgx stdlib (`sql.Open("pgx", dsn)`).
- HTTP-роутер: **chi/v5**.
- Тесты store: **testcontainers-go** (postgres-модуль, эфемерный PostgreSQL).
- Шифрование кредов: **AES-256-GCM**, ключ 32 байта из env (base64).
### Пакеты
```
internal/config — загрузка env (DB DSN, ENC-ключ, listen addr)
internal/crypto — AES-256-GCM Encrypt/Decrypt
internal/store
├─ migrations/*.sql — goose (embed.FS)
├─ queries/*.sql — исходники для sqlc
├─ db/ — сгенерировано sqlc (pgx/v5)
├─ dto/ — TemplateDoc (JSONB) ↔ []model.Record
└─ store.go — Repository-обёртка над db.Queries
internal/provider/registry — map[name]provider.Provider (пока selectel)
internal/service — DomainService: Check / Apply (store + registry + diff)
internal/api — chi-хендлеры + request/response DTO
cmd/server — main: config, pgxpool, goose.Up, registry, роутер
```
### Схема БД (multi-tenant)
```
users(id uuid pk, email text unique, created_at timestamptz)
projects(id uuid pk, user_id uuid fk→users, name text, created_at)
provider_accounts(id uuid pk, project_id uuid fk→projects, provider text,
secret_enc text /*base64(nonce‖ciphertext)*/, comment text, created_at)
templates(id uuid pk, project_id uuid fk→projects, name text,
doc jsonb /*TemplateDoc*/, version int, created_at, updated_at)
domains(id uuid pk, project_id uuid fk→projects, provider_account_id uuid fk→provider_accounts,
zone_name text, zone_id text /*id зоны у провайдера*/, template_id uuid null fk→templates,
created_at)
check_runs(id uuid pk, domain_id uuid fk→domains, result jsonb /*сводка Changeset*/, created_at)
```
- Seed-миграция создаёт один default `user` + `project` (фиксированные UUID) — Фаза 1B без логина.
- `TemplateDoc` = `{ "records": [ {"type","name","ttl","values":[...]} ] }` (sqlc override → `dto.TemplateDoc`).
- Секрет учётки шифруется перед сохранением; расшифровывается только в `service` при вызове
провайдера; **никогда** не сериализуется в API-ответ.
### REST API (`/api/v1`, полный цикл)
| Метод/путь | Назначение |
|---|---|
| `POST/GET/DELETE /projects/{pid}/accounts` | CRUD provider-учёток (secret на вход, не на выход) |
| `POST/GET/PUT/DELETE /projects/{pid}/templates` | CRUD шаблонов (JSONB doc) |
| `POST/GET/DELETE /projects/{pid}/domains` | CRUD доменов |
| `POST /projects/{pid}/domains/import` | ListZones по учётке → завести домены |
| `GET /projects/{pid}/domains/{did}/check` | Changeset (Updates/Prunes/ReadOnly раздельно) |
| `POST /projects/{pid}/domains/{did}/apply` | `{applyUpdates, applyPrunes}` — Prunes только по явному флагу |
### Инварианты безопасности
- `applyPrunes=false` по умолчанию: массовое удаление лишних записей требует явного согласия
(guard из 1A через `Changeset.Prunes()`).
- Секрет учётки не покидает сервер в открытом виде.
- ENC-ключ обязателен при старте; сервис не поднимается без валидного ключа.
### Тестирование 1B
- `crypto` — юниты (round-trip, tamper-detection).
- `store` — интеграционные тесты на testcontainers-go PostgreSQL (миграции + CRUD + JSONB round-trip).
- `service` — мок-провайдер + реальный/мок store (Check/Apply, guard prunes).
- `api` — httptest + мок-service.
### Разбивка
Реализуется **одним планом** `phase1b-persistence-api` (по решению — без под-планов).