From 3d6e3110b30639c25bf877332533b6f95ab4fab1 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Fri, 3 Jul 2026 13:25:49 +0700 Subject: [PATCH] =?UTF-8?q?docs:=20=D0=B4=D0=B5=D1=82=D0=B0=D0=BB=D0=B8?= =?UTF-8?q?=D0=B7=D0=B0=D1=86=D0=B8=D1=8F=20=D0=B4=D0=B8=D0=B7=D0=B0=D0=B9?= =?UTF-8?q?=D0=BD=D0=B0=20=D0=A4=D0=B0=D0=B7=D1=8B=201B=20(persistence=20+?= =?UTF-8?q?=20REST=20API)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Opus 4.8 (1M context) --- .../2026-07-03-dns-autoresolver-design.md | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) diff --git a/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md b/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md index 24c185a..41a3d80 100644 --- a/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md +++ b/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md @@ -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` (по решению — без под-планов).