Files
PnvPanel/docs/architecture.md
T

254 lines
22 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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](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<T>**: явная модель успеха/ошибки вместо исключений для управляемых сценариев.
### 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](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<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 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) │
└─────────────────────────────────────────────────────┘
```