Commit Graph

58 Commits

Author SHA1 Message Date
vasyansk 9ccb304d2e feat(api): read zone records without template + snapshot-to-template
LoadDomain requires a template, so a zone without one could never be
viewed or snapshotted. Adds a template-free path: store.LoadZone /
service.ZoneRef / DomainService.ZoneRecords read a zone's live records
straight from the provider (no diff, no template). GET
/domains/{did}/records exposes read-only viewing; POST
/domains/{did}/template-from-zone snapshots only managed record types
(NS/SOA excluded) into a new template and auto-attaches it to the domain.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-05 12:00:27 +07:00
vasyansk e8e7371f09 fix: drain Identity error body (keep-alive); reject whitespace-only credential fields in form
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 20:36:50 +07:00
vasyansk 568452846a feat(api): structured provider credentials + trial-auth validation on account create
POST /accounts now accepts secret as a provider-specific JSON object
instead of an opaque string, and validates credentials via
provider.Provider.Validate before persisting — invalid credentials get
a generic 400 without ever reaching Store.CreateAccount or echoing the
secret back.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-04 20:12:41 +07:00
vasyansk 32107571d1 feat(selectel): project-scoped IAM auth with token cache; provider Validate
Selectel Cloud DNS v2 requires a project IAM token in X-Auth-Token, not the
raw service-user secret; the previous client sent the static secret directly
and got 401. The client now parses Credentials.Secret as a Creds JSON blob
(username/password/account_id/project_name), exchanges it for a token via
the Identity API (POST /identity/v3/auth/tokens), and caches the token in
memory per-account until 5 minutes before expiry. ListZones/GetRecords/
ApplyChanges send the cached IAM token instead of the raw secret.

provider.Provider gains a Validate(ctx, Credentials) method so a bad account
can be rejected via trial login at creation time; all Provider fakes across
provider/registry/api/service test packages implement it as a no-op stub for
now (Task 2 will make api's mock configurable).

Security: the service-user password is folded into the token cache key via
SHA-256 (never stored in the clear) so a password change invalidates the
cached token; identity errors are generic and never echo the request body.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-04 20:02:36 +07:00
vasyansk 41844d49a0 test(notify): assert per-channel results on decrypt-fail and unknown-type
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-04 16:01:14 +07:00
vasyansk f14916396c feat(notify): per-channel delivery results + accurate notification metrics
Dispatcher.Send now returns []ChannelResult{Type, Err} alongside the
aggregated error, and scheduler.checkDomain increments
NotificationsTotal per channel type/status instead of a single
unconditional IncNotification("dispatch", newStatus) placeholder that
ignored per-channel delivery outcome.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-04 15:56:15 +07:00
vasyansk 504c4c081f fix(phase3): skip templateless domains in scheduler; block CGNAT range in webhook SSRF guard
Domains imported without a template (TemplateID == nil) are a valid,
unconfigured state, not a failure — RunOnce now skips them before
calling checkDomain instead of letting LoadDomain's "no template" error
turn into StatusError and a spammy unknown->error notification.

isBlockedIP now also rejects 100.64.0.0/10 (RFC 6598 carrier-grade
NAT), which net.IP.IsPrivate() does not cover, closing an SSRF gap in
the webhook destination guard (both the pre-request check and the
per-dial check use isBlockedIP).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-04 14:58:09 +07:00
vasyansk 45259b9720 feat(web,api): клиент/хуки расписания/каналов/истории + lastCheckStatus в domainResponse
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-04 14:24:02 +07:00
vasyansk b31f886ae2 feat(server): запуск планировщика, /metrics, graceful shutdown 2026-07-04 14:14:00 +07:00
vasyansk 9475af441e fix(scheduler): убрать двойной SaveCheckRun (Checker персистит), SetDrift через CountDriftDomains, resolved после error 2026-07-04 14:03:49 +07:00
vasyansk 23e02d6804 feat(scheduler): in-process планировщик проверок + смена статуса + уведомления + метрики 2026-07-04 13:53:06 +07:00
vasyansk 070a32717f fix(sec): webhook SSRF-guard через Dialer.Control (закрытие DNS-rebinding TOCTOU) 2026-07-04 13:48:22 +07:00
vasyansk 29f448d4b5 fix(sec): санитизация Telegram-ошибок, SSRF-guard Webhook, чистка логов test-канала, go mod tidy, histogram-бакеты 2026-07-04 13:40:29 +07:00
vasyansk 5a2903ca1e merge 3 wave: worktree-agent-ab476f3616a493a88 2026-07-04 13:32:02 +07:00
vasyansk d3e83ee81f merge 3 wave: worktree-agent-abf50211e004f196f 2026-07-04 13:32:02 +07:00
vasyansk 7d4bf153d7 feat(api): CRUD расписания/каналов + тест-отправка + история проверок
Task 5 Фазы 3: GET/PUT /schedule (дефолт при отсутствии строки, валидация
interval>=60), POST/GET/DELETE /channels (секрет шифруется Cipher, никогда
не возвращается в ответах), POST /channels/{cid}/test через узкий
TestSender-интерфейс (200/502 без утечки секрета), GET /domains/{did}/history
(сначала GetDomain для project-scoping, затем ListCheckRuns — иначе IDOR
через check_runs, который сам по себе не scoped по project).

Добавлены store.GetDomain (обёртка над существующим sqlc-запросом) и
store.ListCheckRuns (новый запрос + sqlc regen) для поддержки истории.
2026-07-04 13:24:50 +07:00
vasyansk e82fb0b13d feat(notify): Telegram/Webhook нотификаторы + Dispatcher по каналам проекта 2026-07-04 13:19:21 +07:00
vasyansk 98d8dee413 feat(metrics): Prometheus registry (checks/drift/notifications) + /metrics handler 2026-07-04 13:18:58 +07:00
vasyansk 6fd847a909 feat(store): schedules, notification_channels, domain last_check_status + методы 2026-07-04 13:10:42 +07:00
vasyansk 901eb51e2a fix(auth): серверная проверка длины пароля, loading-guard и различение ошибок на auth-страницах 2026-07-03 21:33:03 +07:00
vasyansk 4533b0ca25 feat(api): RequireAuth+RequireProjectAccess middleware, IDOR-scope check/apply по projectID 2026-07-03 20:47:40 +07:00
vasyansk 35ffe73ae3 fix(auth): wiring Auth/Sessions, нормализация email, GetUserByID для /me, 409 на дубль, timing-guard логина 2026-07-03 20:29:05 +07:00
vasyansk aa0ef1c6a9 feat(api): auth-хендлеры register/login/logout/me + session cookie 2026-07-03 20:11:00 +07:00
vasyansk a584cf5c37 fix(auth): VerifyPassword валидирует параметры/версию, не паникует на битом хэше 2026-07-03 19:58:54 +07:00
vasyansk 12b7945efc feat(auth): argon2id пароли + session store (sha256 токена) 2026-07-03 19:50:11 +07:00
vasyansk 3bd237d562 feat(store): миграция sessions/password + методы users/sessions/projects
Фаза 2, Task 1: добавлена таблица sessions и nullable password_hash у
users, sqlc-запросы и *Store-обёртки (CreateUser, GetUserByEmail,
CreateProjectForUser, GetProjectOwned, GetUserProject, CreateSession,
GetSessionUser, DeleteSession, RegisterUser в транзакции), интеграционные
тесты на testcontainers.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-03 19:44:36 +07:00
vasyansk 4140847a15 fix(web,server): плейсхолдер dist для воспроизводимой сборки + /api без слэша → API
Коммитим internal/web/dist/index.html как минимальный плейсхолдер, чтобы
//go:embed all:dist находил совпадения на чистом клоне без npm/`make web`
(CRITICAL: go build ./... падал с "pattern all:dist: no matching files
found"). .gitignore теперь игнорирует только реальные build-ассеты
(internal/web/dist/* кроме index.html); `make web` перезаписывает
плейсхолдер настоящей сборкой.

Также чинит MEDIUM: голый /api (без хвостового слэша) уходил в
SPA-fallback вместо API-роутера — вынесен isAPIPath() с явной проверкой
path == "/api", покрыт TestIsAPIPath.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01BwxdSt4reTm7Dj1oxRvpP3
2026-07-03 18:24:24 +07:00
vasyansk bba72cc70f feat(web,server): embed статики SPA + fallback, монтирование в cmd/server 2026-07-03 18:14:18 +07:00
vasyansk ddab6e2162 fix(store,api): идемпотентный import (UNIQUE+ON CONFLICT) + PATCH привязки шаблона к домену 2026-07-03 15:24:08 +07:00
vasyansk 2aca92d070 fix(api): tenant-проверка account/template в CreateDomain (HIGH), атомарный import через транзакцию (MEDIUM) 2026-07-03 15:08:16 +07:00
vasyansk ae6a4d7f4c feat(api): CRUD accounts/templates/domains + import зон (полный цикл), secret не в ответах
Task 9 Фазы 1B: узкий интерфейс TenantStore (внутри store.Account/Template/Domain,
без db.* в api) реализован тонкими обёртками в internal/store/tenant.go; API.Store/
Cipher/Reg добавлены к существующему Svc. Роуты POST/GET/DELETE для accounts/
templates/domains + POST /accounts/{aid}/import (ListZones -> CreateDomain на зону).
accountResponse не содержит секрет ни в каком виде.
2026-07-03 14:53:29 +07:00
vasyansk 763919d23f feat(server): Loader/Recorder на Store, wiring cmd/server (config→migrate→pool→api) 2026-07-03 14:41:09 +07:00
vasyansk 05dc586646 fix(api): 400 на битое тело apply, маскирование internal-ошибок, лимит тела 2026-07-03 14:35:43 +07:00
vasyansk fdf90a7c23 feat(api): chi-роутер, check/apply хендлеры, changeset DTO 2026-07-03 14:28:06 +07:00
vasyansk 8a2d985197 feat(service): Check/Apply оркестрация с guard на prune 2026-07-03 14:22:59 +07:00
vasyansk 635b05361f refactor(store): sqlc override uuid→google/uuid.UUID (убирает pgtype boilerplate) 2026-07-03 14:20:03 +07:00
vasyansk 34bc49ee8c feat(store): sqlc-запросы, dto TemplateDoc, Repository, интеграционные тесты CRUD 2026-07-03 14:08:37 +07:00
vasyansk 9c29d40269 fix(store): postgres.BasicWaitStrategies() — устраняет flaky first-run на macOS
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-07-03 14:04:23 +07:00
vasyansk 5588e10e05 merge 1B wave: worktree-agent-a59d2fd8fdbcc99aa 2026-07-03 14:03:05 +07:00
vasyansk a3bb9474a4 merge 1B wave: worktree-agent-af74a8911eb1561ff 2026-07-03 14:03:05 +07:00
vasyansk 788f1db80e feat(store): goose-миграции схемы + seed default tenant, тест на testcontainers 2026-07-03 13:56:21 +07:00
vasyansk 3b7ed8434b feat(registry): резолвинг провайдера по имени 2026-07-03 13:41:56 +07:00
vasyansk 7c82bafaaa feat(crypto): AES-256-GCM шифрование секретов учёток 2026-07-03 13:41:52 +07:00
vasyansk fc10451340 feat(config): загрузка env-конфига (DSN, ENC-ключ, listen) 2026-07-03 13:35:47 +07:00
vasyansk c0c8e3188d merge backlog: worktree-agent-aca0d858dec169a39 2026-07-03 13:16:27 +07:00
vasyansk b62f7d58cd merge backlog: worktree-agent-a0d4f6f75e1f8f7cd 2026-07-03 13:16:27 +07:00
vasyansk 70f9bc6793 harden(selectel): защита пагинации от неподвижного offset, тест New, документирование disabled 2026-07-03 13:13:24 +07:00
vasyansk c42d242a3b feat(diff): prune-guard Updates()/Prunes() + фиксация семантики dedup 2026-07-03 13:12:10 +07:00
vasyansk 1505997b19 refactor(model): slices.Sort/Equal вместо ручного цикла 2026-07-03 13:11:18 +07:00
vasyansk cb2f826dc2 test(diff): пустой шаблон — массовый Delete управляемых, NS остаётся ReadOnly 2026-07-03 12:57:36 +07:00