From fc5d3cdbae72438c84bfd416fb7e1f8e604e5d26 Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Fri, 3 Jul 2026 19:33:12 +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=202=20(=D0=B0=D0=B2?= =?UTF-8?q?=D1=82=D0=BE=D1=80=D0=B8=D0=B7=D0=B0=D1=86=D0=B8=D1=8F)?= 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 | 75 +++++++++++++++++++ 1 file changed, 75 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 2473f02..b19746c 100644 --- a/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md +++ b/docs/superpowers/specs/2026-07-03-dns-autoresolver-design.md @@ -310,3 +310,78 @@ web/ ### Разбивка Один план `phase1c-react-spa`. + +## Фаза 2 — детализация (авторизация) + +Снимает `DEFAULT_PROJECT_ID`, вводит регистрацию/логин и мультитенантность по пользователям. +Закрывает IDOR из 1B (доступ к чужим ресурсам). + +### Решения + +- **Cookie-сессии в БД**: `httpOnly + Secure + SameSite=Lax` cookie; в БД хранится **хэш** токена + (sha256), не сам токен. Random-токен через `crypto/rand` (32 байта, base64url). +- **Email + пароль**: хэш пароля **argon2id** (`golang.org/x/crypto/argon2`; salt 16 байт, + time=1–3, memory=64MB, threads=4, keyLen=32; формат `$argon2id$...`). +- **Без email-верификации** на старте (подтверждение/сброс пароля — позже). +- **Владелец = пользователь**: без шаринга и ролей. **Один проект на пользователя** (создаётся + при регистрации); много доменов/шаблонов/учёток внутри. Project switcher — позже. +- CSRF: same-origin + `SameSite=Lax` на старте; отдельный CSRF-токен — backlog. + +### БД (миграция 0003) + +``` +ALTER TABLE users ADD COLUMN password_hash text; -- nullable: seed-user остаётся техническим +CREATE TABLE sessions ( + id uuid PRIMARY KEY, + user_id uuid NOT NULL REFERENCES users(id) ON DELETE CASCADE, + token_hash text NOT NULL UNIQUE, -- sha256(token) + expires_at timestamptz NOT NULL, + created_at timestamptz NOT NULL DEFAULT now() +); +CREATE INDEX ON sessions (token_hash); +``` + +### Backend + +``` +internal/auth + ├─ password.go — argon2id Hash(password) / Verify(hash, password) + └─ session.go — SessionStore: Create(userID)→(token,expires), Validate(token)→userID, Delete(token) +internal/api/auth_handlers.go — register / login / logout / me +internal/api/middleware.go — RequireAuth (session→userID в контекст), RequireProjectAccess (pid→owner) +``` + +- `POST /api/v1/auth/register` `{email,password}` — транзакция: создать `user` (password_hash) + + его `project` → создать сессию → `Set-Cookie`. Ответ: `{user, project}` (без хэша). +- `POST /api/v1/auth/login` `{email,password}` — `GetUserByEmail` → argon2 Verify → сессия → cookie. + Ошибка входа — единый ответ 401 (не раскрывать, email или пароль неверны). +- `POST /api/v1/auth/logout` — удалить сессию, очистить cookie. +- `GET /api/v1/auth/me` — из сессии: `{user, project}`; 401 если не авторизован. +- **RequireAuth** middleware: cookie → token → sha256 → `GetSessionByTokenHash` → проверка `expires_at` + → `userID` в контекст; иначе 401. Защищает `/api/v1/projects/*` и `/auth/me`, `/auth/logout`. +- **RequireProjectAccess** middleware на `/projects/{pid}/*`: `GetProject(pid, userID)` — если проект + не принадлежит пользователю → 404 (не 403, чтобы не раскрывать существование). Закрывает IDOR. +- **Рефакторинг 1B под tenant scope**: `LoadDomainFull` получает `AND d.project_id = $2`; + `service.Check/Apply` принимают `projectID`; хендлеры check/apply/CRUD берут `pid` из контекста + (валидированный middleware), а не «как есть». + +### Frontend + +- `AuthContext` (`{user, project, loading, login, register, logout}`) — при старте `GET /auth/me` + (cookie) → user+project или неавторизован. +- API-клиент: `credentials:'include'`; `API_BASE` строится из активного `projectID` **из контекста** + (зашитый `DEFAULT_PROJECT_ID` удаляется); 401 → выход в `/login`. +- Страницы `LoginPage`, `RegisterPage` (email+пароль, zod-валидация); `logout` в `Layout`. +- **Protected routes**: неавторизованный → `/login`; авторизованный на `/login` → `/domains`. + +### Тестирование Фазы 2 + +- `auth` — юниты: argon2id round-trip + неверный пароль; session Create/Validate/Delete, истечение. +- Auth-хендлеры — httptest + store (register создаёт user+project+session; login/logout; me). +- Middleware — RequireAuth (нет/битая/истёкшая сессия → 401); RequireProjectAccess (чужой pid → 404). +- IDOR-регресс: пользователь A не может check/apply/CRUD домен пользователя B. +- Frontend — AuthContext (me/login/logout), protected-route редиректы, 401-обработка, клиент шлёт cookie. + +### Разбивка + +Один план `phase2-auth`.