# 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 │ └───────────────┬───────────────────────────────┬──────────────────────────┘ │ 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) │ └───────────────┬────────────────┘ └───────────────────────────────────────┘ │ ┌───────────▼──────────┐ ┌──────────────────────────┐ │ PostgreSQL │ │ 3x-ui panels (nodes) │ │ (Npgsql / EF Core) │ │ via ThreeXui.Net (HTTP) │ └──────────────────────┘ └──────────────────────────┘ ``` ## Слои (Clean Architecture) Зависимости направлены **внутрь**: `Api → Infrastructure → Application → Domain`. Внутренние слои не знают о внешних. Инверсия зависимостей — через интерфейсы (порты) в `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), а не в хендлерах. > `AppUser` (Identity) живёт в `Infrastructure` (зависит от `IdentityUser`), а домен ссылается > на пользователя по `UserId` (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**: явная модель успеха/ошибки вместо исключений для управляемых сценариев. ### 3. `PnvPanel.Infrastructure` Технические детали и реализации портов. - **Persistence**: `AppDbContext : IdentityDbContext`, реализует `IAppDbContext`; `IEntityTypeConfiguration` для маппингов; миграции 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). ### 4. `PnvPanel.Api` (Presentation) Композиционный корень и транспорт. - **Minimal API** эндпоинты, сгруппированные по фичам (`MapAuthEndpoints`, `MapConfigEndpoints`, `MapAdminEndpoints`). - **SignalR Hubs**: `PanelHub`. - **Telegram-бот**: `TelegramBotHostedService` + хендлеры апдейтов в `Telegram/` (см. отдельный раздел). - **Статика SPA**: раздача собранного фронта из `wwwroot` + SPA-fallback (единый контейнер). - **Middleware**: обработка исключений → ProblemDetails, корреляция запросов, rate limiting. - **DI**: `AddApplication()`, `AddInfrastructure()`, `AddApiServices()` — сборка всех слоёв. - **OpenAPI**: Swashbuckle + Scalar UI; генерация схемы для codegen фронта. ## CQRS - **Команды** меняют состояние, возвращают `Result` / `Result`; выполняются в транзакции (UnitOfWorkBehavior). - **Запросы** только читают; могут ходить в БД проекциями (`Select` в DTO) без загрузки сущностей целиком. - Диспетчер — **собственный тонкий `ISender`**: резолвит хендлер команды/запроса из DI и прогоняет через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — `ICommand`, `IQuery`, `ICommandHandler<,>`, `IQueryHandler<,>`, `IPipelineBehavior<,>`. Пример потока «создать конфиг»: ``` 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 } ``` ## Интеграция с 3x-ui (ThreeXui.Net) `ThreeXui.Net` конфигурируется на **один** `BaseAddress`, а у нас **несколько нод**. Поэтому: - Порт `IXuiPanelGateway` инкапсулирует все операции с панелями и принимает `Node` (или его id): `ListInboundsAsync`, `AddClientAsync`, `UpdateClientAsync`, `RemoveClientAsync`, `GetClientTrafficAsync`, `BuildConnectionStringAsync`, `ProbeAsync`. - `XuiPanelGateway` держит **фабрику/кэш `IXuiClient` per-node** (ключ — `NodeId`), создавая клиента из расшифрованных `NodeCredentials` через `XuiHttpClientFactory`/`HttpClient`. Cookie-session и авто-переавторизация на 401 обеспечиваются самой библиотекой. - Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу. - Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды). - **Реконсиляция дрейфа**: 3x-ui — источник правды по клиентам. При синхронизации сверяем проекцию с панелью: клиент удалён/изменён напрямую в 3x-ui → помечаем конфиг рассинхронизованным (`Disabled`/флаг) и логируем; не «воскрешаем» молча. Наши записи о трафике/статусах обновляем из панели. ## Telegram-бот (presentation-адаптер) Бот — **второй канал доставки** поверх той же Application-логики, что и REST API (не содержит бизнес-правил). Полное описание — в [telegram-bot.md](telegram-bot.md). Ключевое для архитектуры: - Хостится **в процессе Api** как `BackgroundService` (`TelegramBotHostedService`) — это условие для упаковки «фронт+бек в одном контейнере». Транспорт — **long polling** (MVP), webhook — опция. - Обращения к домену — только через собственный `ISender`, теми же командами/запросами, что и веб (`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...). `Telegram.Bot` не проникает в Application/Domain. - **Passwordless-вход**: бот подтверждает `TelegramLoginRequest`, после чего Api выпускает те же JWT/refresh, что и обычный логин (единые правила сессий). Требует предварительной привязки Telegram. ## Realtime (SignalR) - Хаб `PanelHub` (`/hubs/panel`), авторизация по тому же JWT. - **Группы**: `user:{userId}` (личные события), `admins` (события нод/системы). - **События сервер→клиент** (см. [api-design.md](api-design.md)): `configTrafficUpdated`, `configStatusChanged`, `nodeStatusChanged`. - Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`), вызываемый из хендлеров и фоновых сервисов — Application-слой не зависит от SignalR напрямую. ## Фоновые задачи - **TrafficSyncService** — периодически (`PeriodicTimer`) обходит активные ноды, тянет трафик по клиентам, обновляет `VpnConfig`, пишет `TrafficSample` (для графиков), шлёт realtime-события, помечает превышения (`TrafficLimitReached`). - **NodeHealthCheckService** — health-probe нод, обновляет `NodeStatus`, оповещает `admins`. - **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика). - Для MVP — встроенный `BackgroundService`; при росте нагрузки — вынести в Hangfire/Quartz (см. [tech-stack.md](tech-stack.md)). ## Сидирование и старт При старте приложения выполняется идемпотентный сидинг (`DbInitializer`), управляемый переменными окружения (см. [`.env.example`](../.env.example)): - **Системные роли**: `admin` (без лимита конфигов) и `user` (`MaxConfigs = Roles__DefaultUserMaxConfigs`, по умолчанию 3). - **Учётка администратора**: создаётся из `AdminSeed__Username` / `AdminSeed__Password`, если ещё нет; сразу активирована и с ролью `admin`. - **Telegram id админов** (`AdminSeed__TelegramUserIds`) — авторизуют админ-действия в боте и адресуют уведомления (например, запросы на активацию). - **Каталог приложений** (`ClientApp`): если таблица пуста — сидируется из [`seed/client-apps.json`](../seed/client-apps.json) (стартовый набор клиентов по ОС). Дальше — правки через админ-CRUD. Сидинг не перезаписывает существующие данные. Принудительной смены сид-пароля при первом входе **нет** — задавайте сильный `AdminSeed__Password` сразу; сменить пароль можно в приложении. ## RBAC — динамические роли и активация - `AppRole` расширяет `IdentityRole` полем `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 (по `AdminSeed__TelegramUserIds`); решения/блокировки → пользователю (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`). - **Секреты нод**: шифруются `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). - **Валидация входа**: FluentValidation + жёсткая типизация DTO; ошибки — единый `ProblemDetails`. - **CORS**: в проде фронт и бек — один origin (CORS не нужен); в dev — строгий allowlist (`App__CorsOrigins`). - **Аудит**: значимые действия (активация, блокировка, смена роли, отзыв, ноды/инбаунды) пишутся в `AuditLog` (append-only) с источником `Web`/`Telegram`/`System`. ## Обработка ошибок - Управляемые ошибки → `Result`/`Result` → маппинг в HTTP-статус + `ProblemDetails`. - Непредвиденные исключения → глобальный middleware → 500 + корреляция + структурный лог (без утечки деталей). - Доменные исключения (нарушение инвариантов) → 409/422 с понятным сообщением. ## Развёртывание (единый контейнер приложения) По требованию — **один контейнер на всё приложение** (фронт + бек + бот) и отдельный контейнер 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 **не** вводим. - **Миграции**: применяются **автоматически на старте** приложения (в MVP; при масштабировании на несколько инстансов — вынести в отдельный шаг/джобу). - Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения, JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`, `PublicSiteUrl`). ``` [ внешний прокси/шлюз: 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) │ └─────────────────────────────────────────────────────┘ ```