22 KiB
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держит фабрику/кэшIXuiClientper-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) — авторизуют админ-действия в боте и адресуют уведомления (например, запросы на активацию). - Каталог приложений (
ClientApp): если таблица пуста — сидируется изseed/client-apps.json(стартовый набор клиентов по ОС). Дальше — правки через админ-CRUD.
Сидинг не перезаписывает существующие данные. Принудительной смены сид-пароля при первом входе
нет — задавайте сильный AdminSeed__Password сразу; сменить пароль можно в приложении.
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:
node— сборка фронта (pnpm build) →dist/.dotnet sdk—dotnet publishApi; статика фронта копируется вwwwroot.dotnet aspnetruntime — финальный образ запускает 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) │
└─────────────────────────────────────────────────────┘