Files
PnvPanel/docs/architecture.md
T

21 KiB
Raw Blame History

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 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).
  • 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<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).

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<T>; выполняются в транзакции (UnitOfWorkBehavior).
  • Запросы только читают; могут ходить в БД проекциями (Select в DTO) без загрузки сущностей целиком.
  • Диспетчер — собственный тонкий ISender: резолвит хендлер команды/запроса из DI и прогоняет через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — ICommand<T>, IQuery<T>, 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. Ключевое для архитектуры:

  • Хостится в процессе 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): 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).

Сидирование и старт

При старте приложения выполняется идемпотентный сидинг (DbInitializer), управляемый переменными окружения (см. .env.example):

  • Системные роли: admin (без лимита конфигов) и user (MaxConfigs = Roles__DefaultUserMaxConfigs, по умолчанию 3).
  • Учётка администратора: создаётся из AdminSeed__Username / AdminSeed__Password, если ещё нет; сразу активирована и с ролью admin.
  • Telegram id админов (AdminSeed__TelegramUserIds) — авторизуют админ-действия в боте и адресуют уведомления (например, запросы на активацию).

Сидинг не перезаписывает существующие данные; смена пароля админа после первого старта — через приложение.

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 (по 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<T> → маппинг в 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 sdkdotnet publish Api; статика фронта копируется в wwwroot.
    3. dotnet aspnet runtime — финальный образ запускает Api.
  • docker-compose: сервис app (этот образ) + сервис db (PostgreSQL). Всё приложение — в app.
  • Миграции применяются на старте (dev) / отдельным шагом (prod).
  • Конфигурация через appsettings.{Env}.json + переменные окружения / secrets (строка подключения, JWT-ключ, ключ шифрования секретов, Telegram:BotToken, PublicSiteUrl).
┌────────────────── docker-compose ──────────────────┐
│  app  (единый образ)            db  (postgres)      │
│  ├─ REST /api                   └─ том с данными     │
│  ├─ SignalR /hubs/panel                              │
│  ├─ Telegram bot (long polling)                      │
│  └─ статика SPA (wwwroot, fallback → index.html)     │
└─────────────────────────────────────────────────────┘