1. TD-3 Mapster — Application/Mapping/MapsterConfig.cs с
TypeAdapterConfig для Product, Counterparty + collections.
ProductsController.List/Get/GetInternalAsync + CounterpartiesController.
List/Get переведены на .ProjectToType<TDto>(MapsterConfig.Config).
Inline Projection-Expression удалён.
2. SSO scaffold — Microsoft.AspNetCore.Authentication.Google + .MicrosoftAccount
пакеты, условная регистрация в Program.cs (только если ClientId задан).
ExternalAuthController с GET /api/auth/external/{provider} (Challenge или
503 если не настроено), /callback (501 с email — invite-flow TODO),
/providers (булевый список). docs/sso.md инструкция.
3. Stale-data cleanup — HousekeepingJobs расширен:
PruneOrgAuditLogAsync (>90д из Cleanup:OrgAuditLogDays),
PruneDraftsAsync (Supply/RetailSale/Demand старше 30д),
PruneRevokedRefreshTokensAsync (raw SQL DELETE из OpenIddictTokens).
3 новых cron'a в HangfireJobsConfigurator (03:00-03:20 UTC).
4. DB VACUUM automation — DatabaseMaintenanceJobs.VacuumTopTablesAsync:
pg_total_relation_size → топ-5 таблиц → VACUUM (ANALYZE) per table
с замером времени. Default cron еженедельно вс 04:00 UTC.
5. Disk usage monitoring — DiskMonitoringJob ежечасно: DriveInfo.AvailableFreeSpace
на пути из Monitoring:DiskPaths (default "/opt,/var/lib/docker").
<1GB → Telegram-alert на Monitoring:SuperAdminTelegramChatIds.
Anti-spam cooldown 6h. Gauge food_market_disk_free_bytes{mount}.
6. Performance regression detection — ~/nightly-perf-check.sh после
nightly-verify. Парсит /metrics, считает db_avg_ms, сравнивает с
baseline в ~/.fm-watchdog/perf-baseline.json. Δ>30% → Telegram alert
+ baseline НЕ обновляется (sliding window).
7. Public-site analytics placeholder — Astro BaseLayout рендерит
gtag/Yandex.Metrika только если задан PUBLIC_GA_ID / PUBLIC_YM_ID;
иначе <script data-id="REPLACE_ME" data-doc="docs/analytics.md">
маркер. docs/analytics.md с инструкцией подключения.
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
5.7 KiB
SSO — Google и Microsoft
Sprint 20: добавлен скелет SSO. Сейчас можно перейти на consent screen у Google/Microsoft и получить email пользователя, но автоматического создания учётной записи нет — это требует invite-flow от администратора организации (см. multi-tenant ниже).
Как получить keys
- Перейти в Google Cloud Console → APIs & Services → Credentials.
- Create credentials → OAuth client ID.
- Application type: Web application.
- Authorized redirect URIs — добавить:
https://admin.food-market.kz/signin-google(prod)https://test.admin.food-market.kz/signin-google(stage)http://localhost:5081/signin-google(dev)
- Скопировать Client ID и Client secret.
- Положить в
appsettings.Production.json:{ "Authentication": { "Google": { "ClientId": "...", "ClientSecret": "..." } } }
Microsoft
- Перейти в Azure Portal → App registrations → New registration.
- Supported account types: «Accounts in any organizational directory and personal Microsoft accounts».
- Redirect URI (Web): добавить три URL аналогично Google (заменив
signin-google→signin-microsoft). - После создания: Certificates & secrets → New client secret. Скопировать value.
- Overview → Application (client) ID.
- Конфиг:
{ "Authentication": { "Microsoft": { "ClientId": "...", "ClientSecret": "..." } } }
Как использовать
Без настроенных keys
GET /api/auth/external/google → 503 с подсказкой:
{
"error": "SSO для Google не настроено.",
"hint": "Добавьте в appsettings: Authentication:Google:ClientId и :ClientSecret. См. docs/sso.md."
}
GET /api/auth/external/providers → текущее состояние:
{ "google": false, "microsoft": false }
Web-фронт скрывает кнопки SSO когда оба провайдера = false.
С настроенными keys
- Пользователь жмёт «Войти через Google» → фронт делает редирект на
GET /api/auth/external/google. - Сервер возвращает 302 на Google consent screen.
- После consent — Google редиректит на
/signin-google, который обрабатывает ASP.NET middleware и сохраняет identity во временный cookiefm.external. - Middleware вызывает
/api/auth/external/callback?provider=google. - Sprint 20 scaffold: callback возвращает 501 с информацией:
{ "status": "scaffolded", "message": "SSO-callback получен, но автоматическая регистрация ещё не реализована.", "email": "user@example.com", "name": "John Doe", "next": "Попросите администратора организации пригласить вас..." }
Что осталось доделать (после v1)
- Invite-flow: org-админ создаёт Employee запись с email'ом, после чего SSO-callback находит этот email и линкует SSO-identity к существующему User'у.
- Выпуск OpenIddict access+refresh токенов после успешного линка
(использовать тот же flow что и в
AuthController.PasswordGrant). - Связь identity_provider+sub для повторных логинов (новая таблица
external_logins(UserId, Provider, ProviderKey)). - UI: на
/loginрендерить кнопки «Войти через Google/Microsoft» только если/api/auth/external/providersвернулtrueдля провайдера. - Конфликт email: если пользователь с этим email уже есть и привязан к другой org → отказ либо choice «выбрать org».
Multi-tenant специфика
SSO per-organization — реальный пользователь в системе всегда привязан к конкретной org через Employee. SSO-логин по email не определяет org однозначно (один email может работать в нескольких организациях через Employee-приглашения).
Поэтому invite-flow обязателен: SSO не создаёт User'а вслепую, а лишь верифицирует «вы — владелец этого email» и линкует к ранее приглашённому Employee.
Тестирование
Скелет тестируется без реальных keys:
# 503 — провайдер не настроен
curl -i https://test.admin.food-market.kz/api/auth/external/google
# Список — все false
curl -s https://test.admin.food-market.kz/api/auth/external/providers
С реальными keys (нужно настроить в appsettings.Stage.json):
# Редирект на Google consent
curl -i 'https://test.admin.food-market.kz/api/auth/external/google?returnUrl=/dashboard'