254 lines
22 KiB
Markdown
254 lines
22 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 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) │
|
||
└─────────────────────────────────────────────────────┘
|
||
```
|