using System;
using Ban.Sdaid.Notation.Documents;
using Ban.Sdaid.Icd;
namespace Ban.Sdaid.Icd.Arch.Tech.Angular
{
/// <summary>
/// ADR-UI-010: Аутентификация на фронте. Хранение access-token, состав сессионного стора,
/// инициализация профиля при старте, гард аутентификации, HTTP-interceptor и обработка 401/403,
/// экраны входа/выхода. UI не парсит JWT — права приходят из профиля (ADR-UI-011).
/// </summary>
public class ADR_UI_010_Auth : IAdrDocument, IFolder<ArchAngularFolder>
{
public string Name => "ADR-UI-010. Аутентификация";
public string Description =>
@"Простая аутентификация по логину и паролю с access-token (JWT) в HTTP-заголовке Bearer. Токен —
opaque для UI (JWT не парсим). Сессия живёт в сигнальном сторе AuthStore; профиль грузится при старте;
маршруты закрывает AuthGuard; HTTP-interceptor подмешивает токен и обрабатывает 401/403. Закрывает
отложенный «второй режим layout» (экраны входа) и гарды маршрутов.";
public string Version => "0.1";
public string Status => "proposed";
public string[] Comments => new[]
{
"2026-08-20. Фронт-сторона ADR-011 (ABAC). Адаптировано из rtfm ADR-UI-010; закрывает открытые вопросы про auth в ADR-UI-005 (layout) и ADR-UI-006 (гарды).",
};
public Type? Supersedes => null;
public static string S1_Context = $"""
## Контекст и постановка задачи
{nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)} вводит доступ; бэкенд-сторона —
{nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_006_AuthImplementation)} (вход по паролю → JWT,
профиль с UI-claims, отзыв токена). Фронт был спроектирован «с запасом»: основной layout не зависит
от доступа, а «второй режим» (экраны входа) и гарды маршрутов явно отложены до появления ролей
({nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_005_Layout)},
{nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_006_Routing)}). Настало время закрыть это.
Зафиксируем: хранение и срок жизни токена; состав сессионного стора; инициализацию профиля при
старте; гард аутентификации; поведение при 401/403; экраны входа/выхода. Права и их проверку выносим
в {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_011_Claims)}.
""";
public static string S2_DecisionDrivers = $"""
## Драйверы решения
1. **Сохранение сессии** при перезагрузке страницы (F5).
2. **Гарантия инициализации профиля** к моменту, когда первый гард/страница читает claims.
3. **Декларативная защита маршрутов** — гард на корневой обёртке
({nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_006_Routing)}).
4. **Единая точка сессии** — весь код про токен/профиль в одном сторе; страницы про это не думают.
5. **Весь трафик через сгенерированный клиент** ({nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_012_ApiAndModels)}) —
auth-эндпоинты не исключение.
""";
public static string S3_ConsideredOptions = """
## Рассмотренные варианты
- **A. Cookie-сессия (HttpOnly).** Безопаснее к XSS, но требует серверных сессий/CSRF-защиты и хуже
ложится на stateless-JWT бэкенда.
- **B. Access-token в `localStorage`, opaque для UI (выбрано).** Простая модель под JWT-бэкенд; токен
не парсим, права берём из профиля. Стандартный компромисс SPA (требования к XSS жёстче).
- **C. Token в памяти (без хранилища).** Теряется при F5 — неудобно; отклонено.
""";
public static string S4_DecisionOutcome = """
## Решение
Выбран **Вариант B**: access-token в `localStorage`, сессия — в сигнальном сторе `AuthStore`.
### Хранение токена
- Ключ: `icd.accessToken` в `localStorage`. Значение — строка; на UI токен **opaque** (JWT не парсим).
- Очистка при logout / 401: удалить ключ + сбросить сигналы стора.
### `AuthStore` (сессионный стор)
Singleton (`providedIn: 'root'`), сигнальный (ADR-UI-008); имя/файл — по ADR-UI-002 (`auth-store.ts`).
```ts
export interface UserProfile {
userId: string;
fullName: string;
login: string;
claims: readonly string[]; // UI-claims из профиля (см. ADR-UI-011)
}
@Injectable({ providedIn: 'root' })
export class AuthStore {
private readonly _profile = signal<UserProfile | null>(null);
public readonly profile = this._profile.asReadonly();
public readonly isAuthenticated = computed(() => this._profile() !== null);
login(login: string, password: string): Observable<void> { /* ... + loadProfile */ }
loadProfile(): Observable<UserProfile | null> { /* GetUserProfile по токену */ }
clearSession(): void { /* remove token + _profile.set(null) */ }
getAccessToken(): string | null { /* localStorage */ }
}
```
Claims из профиля отдаются в `ClaimService` (ADR-UI-011). Auth-эндпоинты (login/logout/profile)
вызываются через **сгенерированный клиент** (ADR-UI-012), а не прямым `HttpClient`.
### Bootstrap (инициализатор)
После загрузки runtime-конфига (ADR-UI-014)
ещё один `provideAppInitializer`: если токен есть — загрузить профиль (ошибку → `clearSession`);
если нет — показать `/login`. К моменту проверки гардом профиль уже установлен (или явно `null`).
### `AuthGuard`
`CanActivateFn` на корневой обёртке маршрутов: `isAuthenticated()` → пропустить, иначе → `/login`.
Лежит в `src/app/guards/auth.guard.ts`.
### HTTP-interceptor
Один функциональный interceptor `icdAuthHttpInterceptor` (`withInterceptors([...])` в `app.config.ts`;
сейчас `provideHttpClient()` без интерсепторов):
- подмешивает `Authorization: Bearer {token}` **только** к запросам на свой `apiBaseUrl` (чтобы не
утекал на сторонние домены);
- **401** → `clearSession()` + уведомление + `/login`;
- **403** → `/access-denied` **без** очистки сессии (запрещено ≠ не аутентифицирован; иначе петля
редиректов, т.к. `/login` уводит залогиненного обратно).
### Вход / выход
- **Вход** `/login` (второй режим layout, без оболочки — закрывает отложенное в ADR-UI-005): если
уже залогинен → на главную; иначе форма (ADR-UI-009),
на submit `AuthStore.login()` → успех: на главную; ошибка: сообщение из ответа.
- **Выход** `/logout`: технический маршрут, пустой шаблон; зовёт `clearSession()` (и `Logout` на бэке)
→ `/login`. Любая точка «выйти» навигирует на `/logout`, не зовёт `clearSession` напрямую.
### Чего не делаем
- **Не парсим JWT на UI** — права только из профиля (`GetUserProfile`).
- **Не используем cookie/sessionStorage** для токена.
- **Не реализуем refresh-token** — срок жизни задаёт бэк, UI реагирует на 401.
- **Не дублируем navigate-логику выхода** в страницах — только через `/logout`.
""";
public static string S5_PositiveConsequences = """
## Положительные следствия
- Сессия переживает перезагрузку страницы.
- Все auth-инварианты — на уровне инфраструктуры (interceptor + guard + initializer); страницам об
этом думать не нужно.
- Единая точка сессии — замена mock на реальный API/правки контракта не расходятся по коду.
""";
public static string S6_NegativeConsequences = """
## Отрицательные следствия и компромиссы
- `localStorage` доступен любому JS на странице → требования к XSS жёстче (стандартное ограничение
SPA с токеном в хранилище; принимается).
- Один interceptor — точка отказа: ошибка в нём ломает все запросы (покрывается тестами).
- Нет refresh-token: при истечении токена пользователь возвращается на вход (осознанно).
""";
public static string S7_Validation = """
## Проверка
- После входа и F5 пользователь остаётся в системе; шапка показывает его имя.
- 401 от сервера → авто-возврат на `/login` с уведомлением.
- 403 → `/access-denied`, сессия сохранена, без петли редиректов.
- Замена тела `AuthStore.login`/`loadProfile` (mock → API) не требует правок гарда/директив/страниц.
""";
public static string S8_OpenQuestions = """
## Открытые вопросы / отложено
- **Mock-режим** на время отсутствия бэка: `login()`/`loadProfile()` возвращают фиксированный
профиль; при появлении API — замена тела методов, контракт стора не меняется.
- **Refresh-token, OIDC** — отдельными ADR при появлении требований.
- **Многотабовая синхронизация выхода** — слушатель `storage` на ключ `icd.accessToken` (при нужде).
- **Состав сообщений формы входа** — согласуется с бэком; пока показываем то, что пришло.
""";
public static string S99_Related = $"""
## Связанные артефакты
- {nameof(Ban.Sdaid.Icd.Arch.Adr.ADR_011_AbacAuthorization)} — системное решение (ABAC).
- {nameof(Ban.Sdaid.Icd.Arch.Tech.CSharp.ADR_SRV_006_AuthImplementation)} — backend: JWT, профиль, отзыв токена.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_011_Claims)} — claims и проверка прав на UI.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_005_Layout)} — «второй режим» (экраны входа), закрываемый здесь.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_006_Routing)} — гарды маршрутов, закрываемые здесь.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_012_ApiAndModels)} — auth-эндпоинты через сгенерированный клиент.
- {nameof(Ban.Sdaid.Icd.Arch.Tech.Angular.ADR_UI_014_RuntimeConfig)} — initializer и `apiBaseUrl`.
""";
}
}