Some checks failed
CI / Backend (.NET 8) (push) Waiting to run
CI / Web (React + Vite) (push) Waiting to run
CI / POS (WPF, Windows) (push) Waiting to run
Docker API / Build + push API (push) Waiting to run
Docker API / Deploy API on stage (push) Blocked by required conditions
Docker Public / Build + push Public (push) Has been cancelled
Docker Public / Deploy Public on stage (push) Has been cancelled
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>
132 lines
5.7 KiB
Markdown
132 lines
5.7 KiB
Markdown
# SSO — Google и Microsoft
|
||
|
||
Sprint 20: добавлен скелет SSO. Сейчас можно перейти на consent screen
|
||
у Google/Microsoft и получить email пользователя, но автоматического
|
||
создания учётной записи **нет** — это требует invite-flow от
|
||
администратора организации (см. multi-tenant ниже).
|
||
|
||
## Как получить keys
|
||
|
||
### Google
|
||
|
||
1. Перейти в [Google Cloud Console → APIs & Services → Credentials](https://console.cloud.google.com/apis/credentials).
|
||
2. **Create credentials → OAuth client ID**.
|
||
3. Application type: **Web application**.
|
||
4. **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)
|
||
5. Скопировать **Client ID** и **Client secret**.
|
||
6. Положить в `appsettings.Production.json`:
|
||
```json
|
||
{
|
||
"Authentication": {
|
||
"Google": {
|
||
"ClientId": "...",
|
||
"ClientSecret": "..."
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
### Microsoft
|
||
|
||
1. Перейти в [Azure Portal → App registrations](https://portal.azure.com/#blade/Microsoft_AAD_RegisteredApps/ApplicationsListBlade) → **New registration**.
|
||
2. **Supported account types**: «Accounts in any organizational directory and personal Microsoft accounts».
|
||
3. **Redirect URI** (Web): добавить три URL аналогично Google (заменив `signin-google` → `signin-microsoft`).
|
||
4. После создания: **Certificates & secrets → New client secret**. Скопировать **value**.
|
||
5. **Overview → Application (client) ID**.
|
||
6. Конфиг:
|
||
```json
|
||
{
|
||
"Authentication": {
|
||
"Microsoft": {
|
||
"ClientId": "...",
|
||
"ClientSecret": "..."
|
||
}
|
||
}
|
||
}
|
||
```
|
||
|
||
## Как использовать
|
||
|
||
### Без настроенных keys
|
||
|
||
`GET /api/auth/external/google` → **503** с подсказкой:
|
||
```json
|
||
{
|
||
"error": "SSO для Google не настроено.",
|
||
"hint": "Добавьте в appsettings: Authentication:Google:ClientId и :ClientSecret. См. docs/sso.md."
|
||
}
|
||
```
|
||
|
||
`GET /api/auth/external/providers` → текущее состояние:
|
||
```json
|
||
{ "google": false, "microsoft": false }
|
||
```
|
||
|
||
Web-фронт скрывает кнопки SSO когда оба провайдера = false.
|
||
|
||
### С настроенными keys
|
||
|
||
1. Пользователь жмёт «Войти через Google» → фронт делает редирект на
|
||
`GET /api/auth/external/google`.
|
||
2. Сервер возвращает 302 на Google consent screen.
|
||
3. После consent — Google редиректит на `/signin-google`, который
|
||
обрабатывает ASP.NET middleware и сохраняет identity во временный
|
||
cookie `fm.external`.
|
||
4. Middleware вызывает `/api/auth/external/callback?provider=google`.
|
||
5. **Sprint 20 scaffold**: callback возвращает **501** с информацией:
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```bash
|
||
# 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`):
|
||
```bash
|
||
# Редирект на Google consent
|
||
curl -i 'https://test.admin.food-market.kz/api/auth/external/google?returnUrl=/dashboard'
|
||
```
|