1
План stage1 tenancy
stepan edited this page 2026-09-13 00:17:00 +03:00
This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

Перенесено из репозитория (docs/superpowers/plans/2026-09-05-deal-stage1-tenancy.md). Актуальная версия — здесь, в вики.

Дейл (Deal) — Этап 1: Доступ, мультитенантность и фундамент данных Implementation Plan

Исторический документ этапа 1. Актуальное состояние — docs/superpowers/STATUS.md и docs/technical/Техническая-документация-Дейл.md.

Goal: Сделать ядро «Дейл» способным принимать запросы реального фронта (Vue из LeadRadar): аутентификация (login/logout/me/change-password) на пользователях в public, сессии (httpOnly-cookie), tenant-контекст из сессии, физическая схема на тенанта с применением tenant-миграций, первая tenant-таблица (настройки). Этап закладывает паттерн для всех последующих модулей.

Architecture: модульный монолит src/core. HTTP-эндпоинты живут в Deal.Api (папка Endpoints/), вызывают сервисы модулей. Модули содержат домен и прикладные сервисы, НЕ содержат EF. Персистентность — в Deal.Infrastructure (два DbContext: системный public и tenant-схемы) + сущности/конфигурации модулей подключаются туда по одному соглашению (см. Ruling 1).

Spec: docs/architecture/2026-09-05-deal-architecture-design.md §4, 6.1, 8; docs/spec/ТЗ-дейл-новая-архитектура.md §3, 8 (частично); контракт: docs/api/api-map.md (auth); референс-семантика: backend/app/auth.py, backend/app/routers/auth_routes.py, backend/app/main.py, backend/app/config.py.

Global Constraints

  • Проект НЕ git; фиксация — отчёты задач и progress.md плана. Рабочая папка плана: .superpowers/sdd/deal-stage1-tenancy/.
  • .NET 10 SDK, решение собирается с 0 warnings / 0 errors (TreatWarningsAsErrors).
  • Код-стайл: 1 тип = 1 файл; XML-doc для public-контрактов; комментарии на русском; явные модификаторы; настройки через IOptions<T>; без регионов и snake_case-хелперов.
  • namespace Deal.*. Секретов в коде нет (dev-пароль по умолчанию — только seed, из env DEAL_BOOTSTRAP_*).
  • Сущности тенантов — в схеме tenant_<id>; системные — в public. tenantId только из сессии, никогда из тела запроса.
  • LeadRadar-контейнеры и backend/, mlservice/, src/frontend/ не трогаем. Dev-Postgres deal-postgres (:5433) — наша БД.

Зафиксированные решения (Rulings этапа)

  • Ruling 1 (модель персистентности этапа): сущности этапа 1 — в Deal.Infrastructure/Persistence/Entities (POCO, 1 тип = 1 файл), EF-конфигурации — в Deal.Infrastructure/Persistence (рядом с контекстами). Модули (Deal.Modules.*) НЕ содержат EF и НЕ ссылаются на Infrastructure: они объявляют интерфейсы своих хранилищ/сервисов и работают с record-DTO. Реализации интерфейсов — в Infrastructure (паттерн «port & adapter»). Это эталон для последующих модулей; когда у модуля появится богатая логика, его сущности переедут в модуль без изменения контрактов наружу.
  • Ruling 2 (два контекста): DealDbContext остаётся системным (схема public, явный ToTable(...,"public"); таблицы: tenants, users, sessions). Новый TenantDbContext — бессхемная модель (таблицы без указания схемы), живут в схеме через search_path. У TenantDbContext MigrationsHistoryTable получает ИМЯ __TenantMigrationsHistory и схему текущего тенанта на этапе применения (см. Ruling 3).
  • Ruling 3 (применение tenant-миграций): TenantProvisioningService для каждого тенанта: (1) создать схему tenant_<id> (SQL TenantSchemaMigrator.CreateSchemaSql), (2) открыть контекст на строке подключения с Search Path=tenant_<id> и MigrationsHistoryTable("__TenantMigrationsHistory", "tenant_<id>"), (3) Database.Migrate().
  • Ruling 4 (dev-сброс схемы): в public уже применена InitialPublic (пустая таблица tenants — тестовые данные). Пересоздаём миграции системного контекста начисто: удаляем старую миграцию InitialPublic, создаём InitialSystem (tenants+users+sessions), дропаем и пересоздаём dev-БД (deal-postgres). Реальные данные отсутствуют.
  • Ruling 5 (hash пароля): Argon2id через пакет Isopoh.Cryptography.Argon2 (чистый managed, без нативных зависимостей). Формат хранения — encoded-строка из Argon2.Hash(password); проверка Argon2.Verify.
  • Ruling 6 (сессии): токен = 32 случайных байта (Base64Url); в БД хранится SHA-256 токена. Кука deal_session, httpOnly, SameSite=Lax, MaxAge=30 дней, Secure — из конфига (dev=false). Смена пароля удаляет все сессии пользователя и выдаёт свежую (семантика прототипа auth.py).
  • Ruling 7 (эндпоинты): минимальные API-эндпоинты живут в Deal.Api/Endpoints/ (статик-классы MapXxxEndpoints(this IEndpointRouteBuilder)), делегируют в интерфейсы модулей. Конвенция для всех модулей.
  • Ruling 7a (DTO модулей): модуль объявляет record-DTO (папка Application/Models), сериализация наружу — camelCase (ASP.NET default); эндпоинты не видят EF-сущности.
  • Ruling 8 (bootstrap/seed): при старте, если нет тенантов: создаём дефолтного тенанта с ФИКСИРОВАННЫМ id 00000000-0000-0000-0000-000000000001 (схема tenant_000...0001, детерминирована) и пользователя admin (логин/пароль из env DEAL_BOOTSTRAP_LOGIN/PASSWORD, по умолчанию admin/admin) — повторяет ensure_creds прототипа. Seed идемпотентен. Провижининг схемы дефолтного тенанта — тем же TenantProvisioningService.
  • Ruling 9 (первая tenant-таблица): settings (модуль Settings): key text PK, value_json text NOT NULL, updated_at timestamptz NOT NULL. Без неё tenant-миграции нечего применять; таблица понадобится всем модулям.
  • Ruling 10 (DTO/сериализация): ответы — camelCase JSON (ASP.NET default); ошибки — HTTP-код + {"detail": "..."} (семантика FastAPI, см. api.js).

Задачи

Task 1: Карта API

Выполнена (артефакт docs/api/api-map.md). В этом этапе используется секция Auth.

Task 2: Персистентность — системный и tenant-контексты, миграции

Files:

  • Create: src/core/Deal.Infrastructure/Persistence/TenantDbContext.cs
  • Create: src/core/Deal.Infrastructure/Persistence/Entities/UserEntity.cs, SessionEntity.cs, TenantSettingEntity.cs
  • Create: src/core/Deal.Infrastructure/Persistence/UserConfiguration.cs, SessionConfiguration.cs, TenantSettingConfiguration.cs
  • Modify: Deal.Infrastructure/Persistence/DealDbContext.cs (добавить DbSet Users/Sessions)
  • Create: src/core/Deal.Infrastructure/Persistence/TenantDbDesignTimeFactory.cs
  • Delete: старая миграция InitialPublic* в Deal.Infrastructure/Migrations/DealDbContextModelSnapshot.cs — пересоздастся)
  • Migrations: Migrations/InitialSystem (контекст DealDbContext), Migrations/InitialTenant (контекст TenantDbContext) — обе в общей папке Migrations/ (без --output-dir): имена классов миграций и снапшотов (DealDbContextModelSnapshot/TenantDbContextModelSnapshot) не конфликтуют.

Acceptance:

  1. DealDbContext (системный): Tenants, Users, Sessions в схеме public (явная схема в конфигурациях).
  2. TenantDbContext: модель без схемы, таблица settings (см. Ruling 9), MigrationsHistoryTable = __TenantMigrationsHistory (без схемы в модели; схема задаётся при применении).
  3. Сущности — в отдельных файлах (1 тип = 1 файл), конфигурации в отдельных файлах.
  4. Сборка: dotnet build Deal.sln — 0 warnings/0 errors.
  5. Dev-БД пересоздана: public содержит tenants, users, sessions, __EFMigrationsHistory (одна строка InitialSystem).
  6. Tenant-миграция InitialTenant существует и при применении к схеме создаёт там settings и историю — проверка через psql (применение выполняет Task 5; здесь достаточно dotnet ef migrations list и того, что SQL миграции не содержит схемы).
  7. Отчёт: task-2-report.md.

Task 3: Модуль Tenants — домен и прикладные сервисы аутентификации

Files:

  • Create: src/core/Deal.Modules.Tenants/Application/IPasswordHasher.cs, DefaultPasswordHasher.cs (Argon2id, Ruling 5)
  • Create: src/core/Deal.Modules.Tenants/Application/Models/*.cs — record-DTO: UserIdentityDto, SessionDto, LoginResult и т.п. (минимум, что нужно сервисам)
  • Create: src/core/Deal.Modules.Tenants/Application/IAuthStore.cs (поиск пользователя по логину, чтение/создание/удаление сессий, смена пароля — на DTO)
  • Create: src/core/Deal.Modules.Tenants/Application/AuthService.cs (login/logout/changePassword/resolveSession)
  • Create: src/core/Deal.Modules.Tenants/Application/ITenantRepository.cs, TenantService.cs (реестр тенантов; создание тенанта вызывает ITenantProvisioner — интерфейс из модуля)
  • Modify: Deal.Infrastructure — EF-реализации (Persistence/Repositories/AuthStore.cs, TenantRepository.cs) + регистрация DI (Deal.Infrastructure/ServiceCollectionExtensions.cs)
  • Test: tests/Deal.Tests.Unit/PasswordHasherTests.cs, AuthServiceTests.cs (с fake-хранилищем)

Семантика (референс backend/app/auth.py):

  • login: неверные данные → 401 «Неверный логин или пароль»; ok → {ok:true, login}.
  • changePassword: oldPassword неверен → false→400 «Текущий пароль неверен»; новая длина <4 → 400 «Пароль слишком короткий (минимум 4 символа)»; успех → удалить все сессии пользователя.
  • resolveSession по токену (с учётом expires) → login.
  • Сессия живёт 30 дней; «протухшие» сессии удаляются при resolve (очистка).

Acceptance: build 0/0; dotnet test tests/Deal.Tests.Unit — все PASS (было 6 + новые ≥6). Тесты: hash/verify, неверный пароль, смена пароля инвалидирует старые сессии, resolve протухшей сессии → null. Отчёт: task-3-report.md.

Task 4: Эндпоинты auth, middleware сессии, DI, curl-приёмка

Files:

  • Create: src/core/Deal.Api/Endpoints/AuthEndpoints.cs
  • Create: src/core/Deal.Api/Middleware/SessionMiddleware.cs (чтение куки → resolve → TenantContext + CurrentUser в HttpContext.Items; слабые запросы без сессии — дальше, 401 выставляют сами эндпоинты)
  • Create: src/core/Deal.Api/Configuration/CookieOptions.cs (IOptions; Name=deal_session, Days=30, Secure=false)
  • Modify: Deal.Api/Program.cs (CORS dev как в прототипе, cookie-конфиг, DI модулей+инфраструктуры, map auth-группы; статика SPA не нужна)
  • Test/скрипт приёмки: последовательность curl на :5080 (health → login admin/admin → cookie → me → change-password → старый logout/401)

Контракт эндпоинтов (1:1 с прототипом): POST /api/auth/login {login,password} → 200 {ok,login} | 401; POST /api/auth/logout → {ok:true}; GET /api/auth/me → 200 {login,ok} | 401 {detail:"Требуется авторизация"}; POST /api/auth/change-password {oldPassword,newPassword} → {ok:true} | 400.

Acceptance: build 0/0; curl-цепочка проходит (кука выставляется, me работает, после logout — 401). Отчёт: task-4-report.md.

Task 5: Провижининг схем тенантов и bootstrap при старте

Files:

  • Create: src/core/Deal.Infrastructure/Tenancy/TenantProvisioningService.cs (Ruling 3; реализует ITenantProvisioner из модуля)
  • Create: src/core/Deal.Api/Hosting/TenantBootstrapService.cs (IHostedService: seed дефолтного тенанта+admin (Ruling 8), провижининг схем ВСЕХ тенантов при старте; идемпотентно)
  • Modify: Deal.Modules.Tenants/Application/IAuthStore.cs — добавить Task CreateUserAsync(StoredUserDto user, CancellationToken ct) (seed через порт модуля, НЕ через DbContext в Api)
  • Modify: Deal.Infrastructure/Persistence/Repositories/AuthStore.cs — реализовать CreateUserAsync
  • Modify: Deal.Modules.Tenants/Application/TenantService.csCreateTenantAsync(string name, CancellationToken) оставить; при необходимости дать возможность передать явный Guid id (для дефолтного тенанта)
  • Modify: Deal.Infrastructure/ServiceCollectionExtensions.cs — регистрация ITenantProvisioner→TenantProvisioningService
  • Modify: Deal.Api/Program.cs — hosted-сервис вместо StartupSeed; удалить PendingTenantProvisioner
  • Delete: Deal.Api/Hosting/StartupSeed.cs, временная DI-заглушка PendingTenantProvisioner
  • Modify: Deal.Api/Configuration/CookieOptions.csDays по умолчанию = константа сессии модуля (единый источник «30»)

Acceptance: app стартует, seed создан (psql: tenants строка с фикс. id, users admin), схема tenant_<32hex> дефолтного тенанта создана с таблицей settings и __TenantMigrationsHistory (содержит InitialTenant); повторный старт идемпотентен; dotnet build 0/0; все тесты PASS; login admin/admin работает после старта. Отчёт: task-5-report.md.

Task 6: Финал этапа

  • scripts/build.sh, scripts/test.sh — успешны; dotnet ef migrations list — System: InitialSystem, Tenant: InitialTenant.
  • Полная curl-приёмка (health, login, me, logout) + psql-проверка схем.
  • Обновить docs/technical/Техническая-документация-Дейл.md (раздел «Быстрый старт dev» — актуальные шаги: поднять postgres, мигрировать public, запустить API, креды).
  • Отчёт task-6-report.md + финальная строка в progress.md.

Self-Review

  1. Spec coverage: ТЗ §3 (роли/доступ) — Task 3–5; архитектура §4 (мультитенантность) — Task 2, 5; §6.1 (контракт /api, auth) — Task 4; §11 (стандарты) — все задачи.
  2. Placeholder scan: код везде конкретный; референсы на auth.py/api-map точные.
  3. Type consistency: TenantId, ITenantContext, TenantContext, ConnectionStringProvider, TenantProvisioningService, DealDbContext, TenantDbContext, сущности — согласованы между задачами 2–5.
  4. Вне scope этапа 1: kanban/колонки/карточки, проекты, pipeline/очередь/отсев, discovery, сервисы ml/ai/telegram, операторская админка, инвайты, лимиты токенов, валюты — следующие этапы.