- Introduced `IRequiresActivation` interface to enforce activation requirements for multiple commands and queries, ensuring that only activated users can create, edit, or access configurations, news, and applications. - Updated the `RequireActivationBehavior` to handle activation checks uniformly, returning appropriate errors for unauthenticated or inactive users. - Enhanced error handling by adding `NotActivated` error to provide clear feedback for users attempting to access restricted features. - Updated documentation to reflect the new activation requirements and their implications on user access and functionality.
325 lines
31 KiB
Markdown
325 lines
31 KiB
Markdown
# Architecture
|
||
|
||
## Обзор
|
||
|
||
PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **Clean Architecture** с **CQRS**,
|
||
и SPA-фронтенд на **React + Vite**. Backend хранит проекцию домена в **PostgreSQL** и
|
||
оркестрирует панели **3x-ui** через библиотеку **ThreeXui.Net**. Живые обновления — по **SignalR**.
|
||
|
||
```
|
||
┌──────────────────────────────────────────────────────────────────────────┐
|
||
│ React SPA (Vite + TS) │
|
||
│ TanStack Query/Router · shadcn/ui · @microsoft/signalr · zod │
|
||
└───────────────┬───────────────────────────────┬──────────────────────────┘
|
||
│ REST (JSON, JWT Bearer) │ WebSocket (SignalR)
|
||
┌───────────────▼───────────────────────────────▼──────────────────────────┐
|
||
│ PnvPanel.Api (Presentation) │
|
||
│ Minimal API endpoints · SignalR Hubs · Middleware · DI composition root │
|
||
└───────────────┬────────────────────────────────────────────────────────── ┘
|
||
│ ICommand / IQuery (свой диспетчер)
|
||
┌───────────────▼──────────────────────────────────────────────────────────┐
|
||
│ PnvPanel.Application │
|
||
│ Command/Query handlers · Validators · DTOs · Ports (interfaces) · │
|
||
│ Pipeline behaviors · Result<T> │
|
||
└───────────────┬───────────────────────────────┬──────────────────────────┘
|
||
│ implements ports │ uses
|
||
┌───────────────▼───────────────┐ ┌────────────▼──────────────────────────┐
|
||
│ PnvPanel.Infrastructure │ │ PnvPanel.Domain │
|
||
│ 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) │
|
||
│ (Npgsql / EF Core) │ │ via ThreeXui.Net (HTTP) │
|
||
└──────────────────────┘ └──────────────────────────┘
|
||
```
|
||
|
||
## Слои (Clean Architecture)
|
||
|
||
Зависимости направлены **внутрь**: `Api → Infrastructure → Application → Domain`.
|
||
Внутренние слои не знают о внешних. Инверсия зависимостей — через интерфейсы (порты) в
|
||
`Application`, реализуемые в `Infrastructure`.
|
||
|
||
### 1. `PnvPanel.Domain`
|
||
Ядро без внешних зависимостей. Диспетчера доменных событий нет — уведомления и аудит вызываются
|
||
напрямую из CQRS-хендлеров (см. [domain-model.md](domain-model.md#уведомления-и-аудит-без-диспетчера-доменных-событий)).
|
||
|
||
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `ActivationRequest`, `ClientApp`, `AuditLog`,
|
||
`TelegramLinkToken`, `TelegramLoginRequest`, `TrafficSample` (см. [domain-model.md](domain-model.md)).
|
||
- **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды) — единственный VO;
|
||
connection string строит `IXuiPanelGateway` на лету, лимиты трафика не реализованы.
|
||
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`, `ActivationStatus`, `AuditSource`,
|
||
`TelegramLoginStatus`, `OsPlatform`.
|
||
- **Domain Exceptions**: `DomainException` — брошенный при нарушении инварианта в самой сущности
|
||
(например, `Revoke()` уже отозванного конфига); хендлеры такие нарушения не ожидают в норме.
|
||
- Инварианты и бизнес-правила инкапсулированы в сущностях (rich domain model: приватные сеттеры,
|
||
фабричные методы, поведенческие методы), а не в хендлерах.
|
||
|
||
> `AppUser`/`AppRole` (Identity) живут в `Infrastructure` (наследуют `IdentityUser<Guid>`/
|
||
> `IdentityRole<Guid>`), а домен ссылается на пользователя/роль только по `Guid`, чтобы не тащить
|
||
> Identity в ядро.
|
||
|
||
### 2. `PnvPanel.Application`
|
||
Сценарии приложения через CQRS.
|
||
|
||
- **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/AutoMapper).
|
||
- **Pipeline behaviors** (порядок: Logging → Validation → RequireActivation → UnitOfWork):
|
||
`LoggingBehavior`, `ValidationBehavior`, `RequireActivationBehavior` (403 `Auth.NotActivated` для
|
||
запросов с маркером `IRequiresActivation` — конфиги, новости, каталог приложений), `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` напрямую (`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**-эндпоинты, сгруппированные по фичам — 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 (единый контейнер).
|
||
- **Ошибки**: встроенные `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
|
||
|
||
- **Команды** меняют состояние, возвращают `Result` / `Result<T>`; выполняются в транзакции (UnitOfWorkBehavior).
|
||
- **Запросы** только читают; могут ходить в БД проекциями (`Select` в DTO) без загрузки сущностей целиком.
|
||
- Диспетчер — **собственный тонкий `ISender`**: резолвит хендлер команды/запроса из DI и прогоняет
|
||
через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — `ICommand<T>`,
|
||
`IQuery<T>`, `ICommandHandler<,>`, `IQueryHandler<,>`, `IPipelineBehavior<,>`.
|
||
|
||
Пример потока «создать конфиг» (`backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs`):
|
||
```
|
||
POST /api/configs
|
||
→ CreateVpnConfigCommand (IRequiresActivation)
|
||
→ ValidationBehavior (FluentValidation — формат inboundId/label)
|
||
→ RequireActivationBehavior (403 Auth.NotActivated, если аккаунт не активирован)
|
||
→ 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, 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`:
|
||
`ListInboundsAsync`, `AddClientAsync`, `UpdateClientAsync`, `RemoveClientAsync`,
|
||
`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 активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или
|
||
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
|
||
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
|
||
в 3x-ui в обход панели, локальная запись `VpnConfig` продолжит существовать до следующего
|
||
явного действия пользователя/админа (`Revoke`/`Rotate`), которое обнаружит несоответствие по ответу
|
||
гейтвея. Активной сверки/алертинга по дрейфу нет.
|
||
|
||
## Telegram-бот (presentation-адаптер)
|
||
|
||
Бот — **второй канал доставки** поверх той же Application-логики, что и REST API (не содержит
|
||
бизнес-правил). Полное описание — в [telegram-bot.md](telegram-bot.md). Ключевое для архитектуры:
|
||
|
||
- Хостится **в процессе Api** как `BackgroundService` (`TelegramBotHostedService`) — это условие
|
||
для упаковки «фронт+бек в одном контейнере». Транспорт — только **long polling**
|
||
(`ITelegramBotClient.ReceiveAsync`); webhook рассматривался, но не реализован — конфигурации
|
||
`Telegram:Mode`/`WebhookUrl` в коде нет.
|
||
- Обращения к домену — только через собственный `ISender`, теми же командами/запросами, что и веб
|
||
(`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...). `Telegram.Bot`
|
||
не проникает в Application/Domain.
|
||
- **Passwordless-вход**: бот подтверждает `TelegramLoginRequest`, после чего Api выпускает те же
|
||
JWT/refresh, что и обычный логин (единые правила сессий). Требует предварительной привязки Telegram.
|
||
|
||
## Realtime (SignalR)
|
||
|
||
- Хаб `PanelHub` (`/hubs/panel`), авторизация по тому же JWT.
|
||
- **Группы** (`GroupNames` в `Api/Hubs/PanelHub.cs`): `user:{userId}` (личные события), `admins`
|
||
(события нод/системы/активации) — пользователь при подключении добавляется в свою `user:{userId}`
|
||
и, если он админ, дополнительно в `admins`.
|
||
- **События сервер→клиент**: `configTrafficUpdated`, `configStatusChanged`, `nodeStatusChanged`,
|
||
`activationRequested`, `userActivated`, `newsPublished` — точные payload'ы см.
|
||
[api-design.md](api-design.md#signalr--hub-hubspanel). `newsPublished` — единственное
|
||
широковещательное событие (`Clients.All`), а не по группе — новости видны всем без исключения.
|
||
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`, реализация в `Api/Hubs/`),
|
||
вызываемый из хендлеров и фоновых сервисов — Application-слой не зависит от SignalR напрямую.
|
||
|
||
## Фоновые задачи
|
||
|
||
- **TrafficSyncService** — периодически (`PeriodicTimer`) обходит активные ноды, тянет трафик по
|
||
клиентам через `IXuiPanelGateway.GetClientTrafficAsync`, пишет `VpnConfig.UpdateTraffic(...)` и
|
||
`TrafficSample`, шлёт `configTrafficUpdated`. Трафик используется только для отображения — лимиты
|
||
и автоотключение по превышению не реализованы (см. [domain-model.md](domain-model.md)).
|
||
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
|
||
шлёт `nodeStatusChanged` группе `admins`.
|
||
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
|
||
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера
|
||
(см. [tech-stack.md](tech-stack.md)).
|
||
|
||
## Сидирование и старт
|
||
|
||
При старте приложения выполняется идемпотентный сидинг (`DbInitializer`), управляемый переменными
|
||
окружения (см. [`.env.example`](../.env.example)):
|
||
|
||
- **Системные роли**: `admin` (без лимита конфигов) и `user` (`MaxConfigs = Roles__DefaultUserMaxConfigs`, по умолчанию 3).
|
||
- **Учётка администратора**: создаётся из `AdminSeed__Username` / `AdminSeed__Password`, если ещё нет;
|
||
сразу активирована и с ролью `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`. **У пользователя ровно одна роль**;
|
||
квота = `MaxConfigs` его роли (`admin` — без лимита). Админ создаёт роли и меняет роль пользователя.
|
||
- **Активация**: `AppUser.IsActivated`; `ActivationRequest` (с комментарием) обрабатывается админом
|
||
на сайте (`Approve/Reject`-команды) или в Telegram. `ApproveActivation` ставит `IsActivated = true`
|
||
и шлёт realtime-пуш пользователю.
|
||
- **Ролевой доступ к инбаундам**: `Inbound.AllowedRoles` (M:N с `AppRole`); при создании конфига
|
||
доменный инвариант проверяет активацию, квоту и пересечение роли пользователя с `AllowedRoles`.
|
||
- **Понижение роли — грандфазеринг**: смена роли на меньшую квоту разрешена; существующие конфиги
|
||
сохраняются, создание новых блокируется до входа в квоту.
|
||
- **Блокировка пользователя**: `IsBlocked = true` → вход запрещён + все конфиги `Disabled` (отключение
|
||
клиентов в 3x-ui); разблокировка — обратная операция. Пишется в `AuditLog`.
|
||
- Уведомления: запросы активации → группа `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-вход → смена пароля); без
|
||
привязки — сброс админом (`ResetUserPasswordCommand`). Email/SMTP не используются. Смена пароля
|
||
вошедшим — `POST /api/auth/change-password`.
|
||
- **AuthZ**: именованных policy нет — либо `.RequireAuthorization()` (любой вошедший) или
|
||
`.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin))` на группе эндпоинтов, либо
|
||
явная проверка внутри хендлера (владение конфигом — сравнение `VpnConfig.UserId` с `ICurrentUser`).
|
||
Активация — не в хендлере, а в pipeline behavior (`RequireActivationBehavior`, срабатывает на
|
||
запросах с маркером `IRequiresActivation`: конфиги, новости, каталог приложений) — `AuthErrors.NotActivated`.
|
||
- **Секреты нод**: шифруются `ISecretProtector` (Data Protection) перед сохранением; в API/логи не попадают.
|
||
- **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**: не настроен вообще (`AddCors`/`UseCors` в коде нет) — фронт и бек всегда один origin: в
|
||
проде раздаются из одного образа, в dev Vite проксирует `/api`/`/hubs`, так что браузер никогда не
|
||
делает кросс-origin запрос. Отдельного allowlist-конфига для CORS сейчас не существует.
|
||
- **Аудит**: значимые действия (активация, блокировка, смена роли, отзыв, ноды/инбаунды) пишутся в
|
||
`AuditLog` (append-only) с источником `Web`/`Telegram`/`System`.
|
||
|
||
## Обработка ошибок
|
||
|
||
- Управляемые ошибки → `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`, не исключения).
|
||
|
||
## Развёртывание (единый контейнер приложения)
|
||
|
||
По требованию — **один контейнер на всё приложение** (фронт + бек + бот) и отдельный контейнер
|
||
PostgreSQL:
|
||
|
||
- **Единый образ**: ASP.NET Core (`PnvPanel.Api`) обслуживает REST (`/api`), SignalR (`/hubs`),
|
||
хостит Telegram-бота (long polling) **и** раздаёт статику React-SPA (`UseStaticFiles` +
|
||
SPA-fallback на `index.html` для клиентских маршрутов). Фронт и бек — один origin, база API — относительный `/api`.
|
||
- **Multi-stage Dockerfile**:
|
||
1. `node` — сборка фронта (`pnpm build`) → `dist/`.
|
||
2. `dotnet sdk` — `dotnet publish` Api; статика фронта копируется в `wwwroot`.
|
||
3. `dotnet aspnet` runtime — финальный образ запускает Api.
|
||
- **docker-compose**: сервис `app` (этот образ) + сервис `db` (PostgreSQL). Всё приложение — в `app`.
|
||
- **TLS — внешний**: HTTPS терминирует внешний прокси/шлюз (nginx/Traefik/cloud LB администратора),
|
||
вне нашего compose; `app` внутри отдаёт HTTP. Приложение доверяет `X-Forwarded-Proto/For` через
|
||
`ForwardedHeaders`-middleware — иначе Secure-cookie и определение схемы за прокси работать не будут.
|
||
Отдельный nginx/Caddy в compose **не** вводим.
|
||
- **Миграции**: применяются **автоматически на старте** приложения. При масштабировании на несколько
|
||
инстансов миграции стоит вынести в отдельный шаг/джобу.
|
||
- Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения,
|
||
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`).
|
||
|
||
```
|
||
[ внешний прокси/шлюз: TLS termination ] ← HTTPS, вне нашего compose
|
||
│ HTTP + X-Forwarded-*
|
||
┌────────────────── docker-compose ──────────────────┐
|
||
│ app (единый образ) db (postgres) │
|
||
│ ├─ REST /api └─ том с данными │
|
||
│ ├─ SignalR /hubs/panel │
|
||
│ ├─ Telegram bot (long polling) │
|
||
│ └─ статика SPA (wwwroot, fallback → index.html) │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|