Refactor environment configuration and update documentation for MVP status
CI / Backend (build + test) (push) Successful in 1m15s
CI / Frontend (lint + typecheck + build) (push) Successful in 30s

- Removed deprecated Telegram user ID configuration from `.env.example` and added a new setting for admin Telegram user IDs.
- Updated `CLAUDE.md` to reflect the current MVP status, detailing completed features and testing coverage.
- Enhanced `README.md` with quick start instructions for Docker setup and clarified project status.
- Revised API design documentation to include updated error handling and request/response structures.
- Improved frontend documentation to outline the project structure and technologies used.
This commit is contained in:
Leonid Pershin
2026-07-02 14:12:50 +03:00
parent 7e8435ee76
commit cdd67f8e2b
14 changed files with 896 additions and 616 deletions
+147 -80
View File
@@ -25,10 +25,11 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
│ implements ports │ uses
┌───────────────▼───────────────┐ ┌────────────▼──────────────────────────┐
│ PnvPanel.Infrastructure │ │ PnvPanel.Domain │
│ EF Core (Npgsql) · Identity · │ │ Entities · Value Objects · Domain
│ JWT · XuiPanelGateway · │◄──┤ Events · Enums · Domain Exceptions │
│ Background sync · SignalR push│ │ (no external dependencies) │
│ EF Core (Npgsql) · Identity · │ │ Entities · Value Object · Enums ·
│ JWT · XuiPanelGateway · │◄──┤ Domain Exceptions
│ Background sync · Telegram │ │ (no external dependencies) │
└───────────────┬────────────────┘ └───────────────────────────────────────┘
(SignalR-хаб/пуш — в PnvPanel.Api, см. ниже)
┌───────────▼──────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ 3x-ui panels (nodes) │
@@ -43,51 +44,83 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
`Application`, реализуемые в `Infrastructure`.
### 1. `PnvPanel.Domain`
Ядро без внешних зависимостей (маркерный интерфейс доменных событий `IDomainEvent` — свой, в `Domain/Common`).
Ядро без внешних зависимостей. Никакого диспетчера доменных событий нет — это сознательное упрощение
относительно исходного плана, см. ниже.
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `Plan` (см. [domain-model.md](domain-model.md)).
- **Value Objects**: `TrafficLimit`, `NodeCredentials`, `ConnectionLink` и т.п.
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`.
- **Domain Events**: `VpnConfigCreated`, `VpnConfigRevoked`, `TrafficLimitReached`, `NodeWentOffline`.
- **Domain Exceptions**: `DomainException` и специализированные (`ConfigQuotaExceededException`).
- Инварианты и бизнес-правила инкапсулированы в сущностях (rich domain model), а не в хендлерах.
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `ActivationRequest`, `ClientApp`, `AuditLog`,
`TelegramLinkToken`, `TelegramLoginRequest`, `TrafficSample` (см. [domain-model.md](domain-model.md)).
- **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды). Это единственный VO —
`TrafficLimit`/`ConnectionLink` из раннего плана не понадобились (лимиты трафика — backlog,
connection string строит `IXuiPanelGateway` на лету).
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`, `ActivationStatus`, `AuditSource`,
`TelegramLoginStatus`, `OsPlatform`.
- **Domain Exceptions**: `DomainException` — брошенный при нарушении инварианта в самой сущности
(например, `Revoke()` уже отозванного конфига); хендлеры такие нарушения не ожидают в норме.
- Инварианты и бизнес-правила инкапсулированы в сущностях (rich domain model: приватные сеттеры,
фабричные методы, поведенческие методы), а не в хендлерах.
> `AppUser` (Identity) живёт в `Infrastructure` (зависит от `IdentityUser`), а домен ссылается
> на пользователя по `UserId` (Guid), чтобы не тащить Identity в ядро.
> `AppUser`/`AppRole` (Identity) живут в `Infrastructure` (наследуют `IdentityUser<Guid>`/
> `IdentityRole<Guid>`), а домен ссылается на пользователя/роль только по `Guid`, чтобы не тащить
> Identity в ядро.
### 2. `PnvPanel.Application`
Сценарии приложения через CQRS.
- **Commands / Queries** + их **Handlers** (`ICommandHandler<,>` / `IQueryHandler<,>` — свои интерфейсы).
- **Ports (интерфейсы)**: `IAppDbContext`, `IXuiPanelGateway`, `ICurrentUser`, `IJwtTokenService`,
`ISecretProtector`, `IRealtimeNotifier`, `IDateTime`.
- **Validators**: FluentValidation на каждую команду/запрос.
- **DTOs** и профили маппинга (Mapster).
- **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция на команду), `AuthorizationBehavior`.
- **Result<T>**: явная модель успеха/ошибки вместо исключений для управляемых сценариев.
- **Commands / Queries** + их **Handlers** (`ICommandHandler<,>` / `IQueryHandler<,>` — свои интерфейсы),
организованы по фичам (`Auth/Login/`, `Configs/Create/`, `Admin/Nodes/`, ...).
- **Ports (интерфейсы)**: `IAppDbContext`, `IXuiPanelGateway`, `ICurrentUser`, `IIdentityService`,
`ISecretProtector`, `IRealtimeNotifier`, `ITelegramNotifier`, `IRoleService`.
- **Validators**: FluentValidation на команды, где есть что проверять помимо типов (не на все — см.
[backend-conventions.md](backend-conventions.md)).
- **DTO**: плоские `record`, конвертация из сущностей — статический метод `FromDomain(...)` на самом
DTO. Mapster из исходного плана не пригодился — при таком числе полей ручной маппинг читается
не хуже конфига маппера и не создаёт лишней зависимости.
- **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция +
`SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` нет — авторизация (роль,
активация) — это либо `RequireAuthorization()`/`RequireRole(...)` на эндпоинте, либо явная проверка
в начале хендлера (например, «инбаунд доступен роли пользователя»).
- **Result<T>**: явная модель успеха/ошибки (`Result`/`Result<T>`, `Error` с `ErrorType`) вместо
исключений для управляемых сценариев.
### 3. `PnvPanel.Infrastructure`
Технические детали и реализации портов.
- **Persistence**: `AppDbContext : IdentityDbContext<AppUser, AppRole, Guid>`, реализует `IAppDbContext`;
`IEntityTypeConfiguration<T>` для маппингов; миграции EF Core; репозитории только там, где нужны
(в основном хендлеры работают через `IAppDbContext` напрямую).
- **Identity & Auth**: ASP.NET Core Identity, `JwtTokenService` (access + refresh), хранение refresh-токенов.
- **3x-ui интеграция**: `XuiPanelGateway : IXuiPanelGateway` поверх `ThreeXui.Net`; фабрика клиентов per-node (см. ниже).
- **Realtime**: `SignalRRealtimeNotifier : IRealtimeNotifier` (пуш в хабы).
- **Background jobs**: `TrafficSyncService`, `NodeHealthCheckService` (`BackgroundService` + `PeriodicTimer`).
- **Secrets**: `DataProtectionSecretProtector : ISecretProtector` (шифрование паролей нод at-rest).
`IEntityTypeConfiguration<T>` для маппингов; миграции EF Core. Репозиториев нет — хендлеры работают
через `IAppDbContext` напрямую (`DbSet<T>` + LINQ).
- **Identity & Auth**: ASP.NET Core Identity, `JwtTokenService` (access + refresh), `RefreshTokenService`
(хранение/ротация/отзыв refresh-токенов), `RoleService`, `DbInitializer` (сидинг).
- **3x-ui интеграция**: `XuiPanelGateway : IXuiPanelGateway` поверх `ThreeXui.Net`; кэш клиентов per-node
внутри самого гейтвея (см. ниже — отдельного класса-фабрики нет).
- **Background jobs**: `TrafficSyncService`, `NodeHealthCheckService`, `TrafficRetentionService`
(`BackgroundService` + `PeriodicTimer`).
- **Secrets**: `DataProtectionSecretProtector : ISecretProtector` (шифрование паролей нод at-rest,
ASP.NET Core Data Protection, key-ring на томе `dp_keys`).
- **Telegram**: `TelegramNotifier : ITelegramNotifier` — отправка DM-уведомлений через `ITelegramBotClient`.
> **SignalR-пуш физически лежит в `PnvPanel.Api/Hubs/`, не в `Infrastructure`.**
> `SignalRRealtimeNotifier : IRealtimeNotifier` нужен `IHubContext<PanelHub>`, а сам `PanelHub`
> определён там же — не было смысла тащить эту связку через слой. `Application` всё равно видит
> только порт `IRealtimeNotifier`, так что граница зависимостей не нарушена.
### 4. `PnvPanel.Api` (Presentation)
Композиционный корень и транспорт.
- **Minimal API** эндпоинты, сгруппированные по фичам (`MapAuthEndpoints`, `MapConfigEndpoints`, `MapAdminEndpoints`).
- **SignalR Hubs**: `PanelHub`.
- **Telegram-бот**: `TelegramBotHostedService` + хендлеры апдейтов в `Telegram/` (см. отдельный раздел).
- **Minimal API**-эндпоинты, сгруппированные по фичам — 12 файлов в `Endpoints/`
(`AuthEndpoints`, `ActivationEndpoints`, `ConfigEndpoints`, `AppEndpoints`, `SubscriptionEndpoints`,
`TelegramEndpoints`, `AdminUserEndpoints`, `AdminAppEndpoints`, `AdminStatsEndpoints`, `NodeEndpoints`,
`InboundEndpoints`, `RoleEndpoints`); полный список маршрутов — [api-design.md](api-design.md).
Каждый эндпоинт аннотирован `.Produces<T>()`, чтобы OpenAPI-схема полностью описывала тело ответа
(нужно для `pnpm gen:api` на фронте).
- **SignalR Hubs**: `PanelHub` (`Hubs/`).
- **Telegram-бот**: `TelegramBotHostedService` + `PnvBotUpdateHandler` в `Telegram/` (см. отдельный раздел).
- **Статика SPA**: раздача собранного фронта из `wwwroot` + SPA-fallback (единый контейнер).
- **Middleware**: обработка исключений → ProblemDetails, корреляция запросов, rate limiting.
- **DI**: `AddApplication()`, `AddInfrastructure()`, `AddApiServices()` — сборка всех слоёв.
- **OpenAPI**: Swashbuckle + Scalar UI; генерация схемы для codegen фронта.
- **Ошибки**: встроенные `AddProblemDetails()` + `UseExceptionHandler()` (ASP.NET Core, без кастомного
middleware) конвертируют необработанные исключения в `application/problem+json`.
- **DI**: `AddApplication()` (Application), `AddInfrastructure()` (Infrastructure) + прямая регистрация
в `Program.cs` для того, что специфично Api-слою (SignalR, Telegram-клиент, rate limiting).
- **OpenAPI**: нативный `Microsoft.AspNetCore.OpenApi` (`/openapi/v1.json`) + `Scalar.AspNetCore`
UI (`/scalar`) — без Swashbuckle.
## CQRS
@@ -97,36 +130,44 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — `ICommand<T>`,
`IQuery<T>`, `ICommandHandler<,>`, `IQueryHandler<,>`, `IPipelineBehavior<,>`.
Пример потока «создать конфиг»:
Пример потока «создать конфиг» (`backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs`):
```
POST /api/configs
→ CreateVpnConfigCommand
→ ValidationBehavior (FluentValidation)
AuthorizationBehavior (роль/владение)
→ CreateVpnConfigHandler
· проверяет квоту пользователя (домен)
· IXuiPanelGateway.AddClientAsync(node, inbound, spec) // 3x-ui
· создаёт VpnConfig, сохраняет через IAppDbContext
· публикует VpnConfigCreated (domain event)
→ UnitOfWorkBehavior (commit)
→ 201 Created { id, link, subscriptionUrl }
→ ValidationBehavior (FluentValidation — формат inboundId/label/deviceLimit)
CreateVpnConfigCommandHandler
· проверяет активацию + роль инбаунда (доменные проверки)
· SELECT pg_advisory_xact_lock(hashtext(userId)) — сериализует параллельные создания
· пересчитывает текущее число активных конфигов и сверяет с AppRole.MaxConfigs
· IXuiPanelGateway.AddClientAsync(node, inbound, ...) // 3x-ui, получает ClientExternalId
· VpnConfig.Create(...) + AssignRemoteClient(id), сохраняет через IAppDbContext
· при сбое SaveChanges после успешного AddClientAsync — компенсация (RemoveClientAsync)
→ UnitOfWorkBehavior (commit транзакции)
→ 200 OK VpnConfigDto { id, label, protocol, location, deviceLimit, usedUpBytes, usedDownBytes,
expiresAt, status, createdAt }
```
Ссылка подключения в ответ создания **не входит** — фронт запрашивает её отдельно,
`GET /api/configs/{id}/link`, по кнопке на карточке конфига (см. [api-design.md](api-design.md)).
## Интеграция с 3x-ui (ThreeXui.Net)
`ThreeXui.Net` конфигурируется на **один** `BaseAddress`, а у нас **несколько нод**. Поэтому:
- Порт `IXuiPanelGateway` инкапсулирует все операции с панелями и принимает `Node` (или его id):
- Порт `IXuiPanelGateway` инкапсулирует все операции с панелями и принимает `Node`:
`ListInboundsAsync`, `AddClientAsync`, `UpdateClientAsync`, `RemoveClientAsync`,
`GetClientTrafficAsync`, `BuildConnectionStringAsync`, `ProbeAsync`.
- `XuiPanelGateway` держит **фабрику/кэш `IXuiClient` per-node** (ключ — `NodeId`), создавая клиента
из расшифрованных `NodeCredentials` через `XuiHttpClientFactory`/`HttpClient`. Cookie-session и
авто-переавторизация на 401 обеспечиваются самой библиотекой.
`GetClientTrafficAsync`, `BuildConnectionStringAsync`, `ProbeAsync`, `ValidateBaseAddress`,
`InvalidateClient(nodeId)` (вызывается после смены креденшлов ноды).
- `XuiPanelGateway` — единственная реализация, держит `ConcurrentDictionary<Guid, Lazy<IXuiClient>>`
(ключ — `NodeId`), создавая клиента из расшифрованных `NodeCredentials` лениво при первом обращении
к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`.
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
- **Реконсиляция дрейфа**: 3x-ui — источник правды по клиентам. При синхронизации сверяем проекцию
с панелью: клиент удалён/изменён напрямую в 3x-ui → помечаем конфиг рассинхронизованным
(`Disabled`/флаг) и логируем; не «воскрешаем» молча. Наши записи о трафике/статусах обновляем из панели.
- **Дрейф с 3x-ui в MVP не реконсилируется активно**: `TrafficSyncService` при недоступной ноде или
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
в 3x-ui в обход панели, локальная запись `VpnConfig` продолжит существовать до следующего
явного действия пользователя/админа (`Revoke`/`Rotate`), которое обнаружит несоответствие по ответу
гейтвея. Активная сверка/алертинг по дрейфу — задел на будущее, не реализовано.
## Telegram-бот (presentation-адаптер)
@@ -134,7 +175,9 @@ POST /api/configs
бизнес-правил). Полное описание — в [telegram-bot.md](telegram-bot.md). Ключевое для архитектуры:
- Хостится **в процессе Api** как `BackgroundService` (`TelegramBotHostedService`) — это условие
для упаковки «фронт+бек в одном контейнере». Транспорт — **long polling** (MVP), webhook — опция.
для упаковки «фронт+бек в одном контейнере». Транспорт — только **long polling**
(`ITelegramBotClient.ReceiveAsync`); webhook рассматривался, но не реализован — конфигурации
`Telegram:Mode`/`WebhookUrl` в коде нет.
- Обращения к домену — только через собственный `ISender`, теми же командами/запросами, что и веб
(`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...). `Telegram.Bot`
не проникает в Application/Domain.
@@ -144,21 +187,25 @@ POST /api/configs
## Realtime (SignalR)
- Хаб `PanelHub` (`/hubs/panel`), авторизация по тому же JWT.
- **Группы**: `user:{userId}` (личные события), `admins` (события нод/системы).
- **События сервер→клиент** (см. [api-design.md](api-design.md)): `configTrafficUpdated`,
`configStatusChanged`, `nodeStatusChanged`.
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`), вызываемый из хендлеров и
фоновых сервисов — Application-слой не зависит от SignalR напрямую.
- **Группы** (`GroupNames` в `Api/Hubs/PanelHub.cs`): `user:{userId}` (личные события), `admins`
(события нод/системы/активации) — пользователь при подключении добавляется в свою `user:{userId}`
и, если он админ, дополнительно в `admins`.
- **События сервер→клиент**: `configTrafficUpdated`, `configStatusChanged`, `nodeStatusChanged`,
`activationRequested`, `userActivated` — точные payload'ы см. [api-design.md](api-design.md#signalr--hub-hubspanel).
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`, реализация в `Api/Hubs/`),
вызываемый из хендлеров и фоновых сервисов — Application-слой не зависит от SignalR напрямую.
## Фоновые задачи
- **TrafficSyncService** — периодически (`PeriodicTimer`) обходит активные ноды, тянет трафик по
клиентам, обновляет `VpnConfig`, пишет `TrafficSample` (для графиков), шлёт realtime-события,
помечает превышения (`TrafficLimitReached`).
- **NodeHealthCheckService** — health-probe нод, обновляет `NodeStatus`, оповещает `admins`.
клиентам через `IXuiPanelGateway.GetClientTrafficAsync`, пишет `VpnConfig.UpdateTraffic(...)` и
`TrafficSample`, шлёт `configTrafficUpdated`. Трафик используется только для отображения — лимиты
и автоотключение по превышению не реализованы (см. [domain-model.md](domain-model.md)).
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
шлёт `nodeStatusChanged` группе `admins`.
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
- Для MVP — встроенный `BackgroundService`; при росте нагрузки — вынести в Hangfire/Quartz
(см. [tech-stack.md](tech-stack.md)).
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера — для
нагрузки MVP этого достаточно (см. [tech-stack.md](tech-stack.md)).
## Сидирование и старт
@@ -167,15 +214,19 @@ POST /api/configs
- **Системные роли**: `admin` (без лимита конфигов) и `user` (`MaxConfigs = Roles__DefaultUserMaxConfigs`, по умолчанию 3).
- **Учётка администратора**: создаётся из `AdminSeed__Username` / `AdminSeed__Password`, если ещё нет;
сразу активирована и с ролью `admin`.
- **Telegram id админов** (`AdminSeed__TelegramUserIds`) — авторизуют админ-действия в боте и
адресуют уведомления (например, запросы на активацию).
сразу активирована и с ролью `admin`. Seed-админ **не привязывается к Telegram автоматически**
привязка делается вручную в UI, как у любого пользователя.
- **Каталог приложений** (`ClientApp`): если таблица пуста — сидируется из
[`seed/client-apps.json`](../seed/client-apps.json) (стартовый набор клиентов по ОС). Дальше — правки через админ-CRUD.
Сидинг не перезаписывает существующие данные. Принудительной смены сид-пароля при первом входе
**нет** — задавайте сильный `AdminSeed__Password` сразу; сменить пароль можно в приложении.
Отдельно от сидинга — **Telegram id админов** (`Telegram__AdminTelegramUserIds`, через запятую)
читаются `TelegramOptions` **напрямую при каждой проверке** (не пишутся в БД): именно этот список
авторизует нажатие «Активировать/Отклонить» в боте и определяет, кому слать уведомления о новых
запросах активации.
## RBAC — динамические роли и активация
- `AppRole` расширяет `IdentityRole<Guid>` полем `MaxConfigs`. **У пользователя ровно одна роль**;
@@ -189,34 +240,50 @@ POST /api/configs
сохраняются, создание новых блокируется до входа в квоту.
- **Блокировка пользователя**: `IsBlocked = true` → вход запрещён + все конфиги `Disabled` (отключение
клиентов в 3x-ui); разблокировка — обратная операция. Пишется в `AuditLog`.
- Уведомления: запросы активации → группа `admins` (SignalR) + Telegram (по `AdminSeed__TelegramUserIds`);
решения/блокировки → пользователю (SignalR + Telegram-DM, если привязан).
- Уведомления: запросы активации → группа `admins` (SignalR) + Telegram (по
`Telegram__AdminTelegramUserIds`); решения/блокировки → пользователю (SignalR + Telegram-DM, если привязан).
## Безопасность
- **AuthN**: ASP.NET Core Identity + JWT, **вход по `UserName`** (email в системе не используется).
Access-token — короткий TTL (in-memory на клиенте); refresh-token — httpOnly Secure cookie,
ротация при использовании, хранение хэша в БД.
- **Восстановление пароля**: через привязанный Telegram (passwordless-вход → смена пароля, либо
reset-флоу в боте); без привязки — сброс админом (`ResetUserPasswordCommand`). Email/SMTP не используются.
Смена пароля вошедшим — `POST /api/auth/change-password`.
- **AuthZ**: роли (`admin`/`user`/кастомные) + policy-based (`OwnsConfig`, `RequireAdmin`,
`RequireActivated`).
- **Восстановление пароля**: через привязанный Telegram (passwordless-вход → смена пароля); без
привязки — сброс админом (`ResetUserPasswordCommand`). Email/SMTP не используются. Смена пароля
вошедшим — `POST /api/auth/change-password`.
- **AuthZ**: именованных policy нет — либо `.RequireAuthorization()` (любой вошедший) или
`.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin))` на группе эндпоинтов, либо
явная проверка внутри хендлера (владение конфигом — сравнение `VpnConfig.UserId` с `ICurrentUser`;
активация — `ConfigErrors.NotActivated`).
- **Секреты нод**: шифруются `ISecretProtector` (Data Protection) перед сохранением; в API/логи не попадают.
- **CSRF**: refresh-cookie — `SameSite=Strict/Lax`, `Secure`, `HttpOnly`; для cookie-based refresh —
анти-CSRF токен. Мутации — только по Bearer access-токену, не по cookie.
- **Brute-force**: Identity lockout по числу неудачных входов; rate-limit на `/auth/*`.
- **Rate limiting**: на `/auth/*`, создание/ротацию конфигов, запросы активации и Telegram (встроенный `RateLimiter` .NET).
- **CSRF**: явного анти-CSRF токена нет — все мутации API читают авторизацию только из
`Authorization: Bearer` (JS должен явно прочитать access-token из памяти и подставить заголовок,
чужой сайт этого сделать не может). Refresh-cookie (`pnv_refresh_token`) — единственное, что браузер
шлёт автоматически; она `HttpOnly`, `SameSite=Strict`, `Path=/api/auth`, и `Secure` выставляется по
`HttpContext.Request.IsHttps` (учитывает `ForwardedHeaders` за прокси) — этого достаточно, т.к. сама
по себе она не даёт мутировать данные, только обменивается на access-token эндпоинтом `/api/auth/refresh`.
- **Brute-force**: Identity lockout по числу неудачных входов; rate-limit на `/auth/*` (настраиваемый
лимит, `RateLimiting:AuthPermitLimit`, по умолчанию 20 запросов/мин).
- **Rate limiting**: встроенный `RateLimiter` .NET, один fixed-window лимит (`RateLimiting:AuthPermitLimit`,
по умолчанию 20/мин) применён к `/api/auth/*`, `/api/auth/telegram/*` и `/sub/{token}`; остальные
эндпоинты (в т.ч. создание конфигов) им не покрыты.
- **Валидация входа**: FluentValidation + жёсткая типизация DTO; ошибки — единый `ProblemDetails`.
- **CORS**: в проде фронт и бек один origin (CORS не нужен); в dev — строгий allowlist (`App__CorsOrigins`).
- **CORS**: не настроен вообще (`AddCors`/`UseCors` в коде нет) — фронт и бек всегда один origin: в
проде раздаются из одного образа, в dev Vite проксирует `/api`/`/hubs`, так что браузер никогда не
делает кросс-origin запрос. Отдельного allowlist-конфига для CORS сейчас не существует.
- **Аудит**: значимые действия (активация, блокировка, смена роли, отзыв, ноды/инбаунды) пишутся в
`AuditLog` (append-only) с источником `Web`/`Telegram`/`System`.
## Обработка ошибок
- Управляемые ошибки → `Result`/`Result<T>` → маппинг в HTTP-статус + `ProblemDetails`.
- Непредвиденные исключения → глобальный middleware → 500 + корреляция + структурный лог (без утечки деталей).
- Доменные исключения (нарушение инвариантов) → 409/422 с понятным сообщением.
- Управляемые ошибки → `Result`/`Result<T>` (`ResultExtensions.ToHttpResult`) → маппинг `ErrorType` в
HTTP-статус + `application/problem+json` (400/401/403/404/409/422).
- Непредвиденные исключения → встроенные `AddProblemDetails()` + `UseExceptionHandler()` → 500 без
утечки деталей + `Serilog` request-логирование (`UseSerilogRequestLogging`, обогащение —
`Enrich.FromLogContext()`). Сквозного `CorrelationId`/явного обогащения `UserId`/`NodeId`/`ConfigId`
в логах пока нет — задел на будущее, а не то, на что стоит полагаться при расследовании инцидентов сегодня.
- Доменные исключения (нарушение инвариантов) — `DomainException`, ожидаются только как баг, а не
штатный путь (штатные отказы — через `Result.Failure`, не исключения).
## Развёртывание (единый контейнер приложения)
@@ -238,7 +305,7 @@ PostgreSQL:
- **Миграции**: применяются **автоматически на старте** приложения (в MVP; при масштабировании на
несколько инстансов — вынести в отдельный шаг/джобу).
- Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения,
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`, `PublicSiteUrl`).
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`).
```
[ внешний прокси/шлюз: TLS termination ] ← HTTPS, вне нашего compose