Table of Contents
- Дейл (Deal) — Этап 1: Доступ, мультитенантность и фундамент данных Implementation Plan
- Global Constraints
- Зафиксированные решения (Rulings этапа)
- Задачи
- Task 1: Карта API
- Task 2: Персистентность — системный и tenant-контексты, миграции
- Task 3: Модуль Tenants — домен и прикладные сервисы аутентификации
- Task 4: Эндпоинты auth, middleware сессии, DI, curl-приёмка
- Task 5: Провижининг схем тенантов и bootstrap при старте
- Task 6: Финал этапа
- Self-Review
Перенесено из репозитория (
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, из envDEAL_BOOTSTRAP_*). - Сущности тенантов — в схеме
tenant_<id>; системные — вpublic.tenantIdтолько из сессии, никогда из тела запроса. - LeadRadar-контейнеры и
backend/,mlservice/,src/frontend/не трогаем. Dev-Postgresdeal-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. УTenantDbContextMigrationsHistoryTableполучает ИМЯ__TenantMigrationsHistoryи схему текущего тенанта на этапе применения (см. Ruling 3). - Ruling 3 (применение tenant-миграций):
TenantProvisioningServiceдля каждого тенанта: (1) создать схемуtenant_<id>(SQLTenantSchemaMigrator.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(логин/пароль из envDEAL_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:
DealDbContext(системный):Tenants,Users,Sessionsв схемеpublic(явная схема в конфигурациях).TenantDbContext: модель без схемы, таблицаsettings(см. Ruling 9),MigrationsHistoryTable=__TenantMigrationsHistory(без схемы в модели; схема задаётся при применении).- Сущности — в отдельных файлах (1 тип = 1 файл), конфигурации в отдельных файлах.
- Сборка:
dotnet build Deal.sln— 0 warnings/0 errors. - Dev-БД пересоздана:
publicсодержитtenants,users,sessions,__EFMigrationsHistory(одна строкаInitialSystem). - Tenant-миграция
InitialTenantсуществует и при применении к схеме создаёт тамsettingsи историю — проверка через psql (применение выполняет Task 5; здесь достаточноdotnet ef migrations listи того, что SQL миграции не содержит схемы). - Отчёт:
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.cs—CreateTenantAsync(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.cs—Daysпо умолчанию = константа сессии модуля (единый источник «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
- Spec coverage: ТЗ §3 (роли/доступ) — Task 3–5; архитектура §4 (мультитенантность) — Task 2, 5; §6.1 (контракт /api, auth) — Task 4; §11 (стандарты) — все задачи.
- Placeholder scan: код везде конкретный; референсы на
auth.py/api-map точные. - Type consistency:
TenantId,ITenantContext,TenantContext,ConnectionStringProvider,TenantProvisioningService,DealDbContext,TenantDbContext, сущности — согласованы между задачами 2–5. - Вне scope этапа 1: kanban/колонки/карточки, проекты, pipeline/очередь/отсев, discovery, сервисы ml/ai/telegram, операторская админка, инвайты, лимиты токенов, валюты — следующие этапы.