Refactor environment configuration and update documentation for MVP status
- Removed deprecated Telegram user ID configuration from `.env.example` and added a new setting for admin Telegram user IDs. - Updated `CLAUDE.md` to reflect the current MVP status, detailing completed features and testing coverage. - Enhanced `README.md` with quick start instructions for Docker setup and clarified project status. - Revised API design documentation to include updated error handling and request/response structures. - Improved frontend documentation to outline the project structure and technologies used.
This commit is contained in:
+147
-80
@@ -25,10 +25,11 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
|
||||
│ 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) │
|
||||
│ EF Core (Npgsql) · Identity · │ │ Entities · Value Object · Enums · │
|
||||
│ JWT · XuiPanelGateway · │◄──┤ Domain Exceptions │
|
||||
│ Background sync · Telegram │ │ (no external dependencies) │
|
||||
└───────────────┬────────────────┘ └───────────────────────────────────────┘
|
||||
(SignalR-хаб/пуш — в PnvPanel.Api, см. ниже)
|
||||
│
|
||||
┌───────────▼──────────┐ ┌──────────────────────────┐
|
||||
│ PostgreSQL │ │ 3x-ui panels (nodes) │
|
||||
@@ -43,51 +44,83 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
|
||||
`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), а не в хендлерах.
|
||||
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `ActivationRequest`, `ClientApp`, `AuditLog`,
|
||||
`TelegramLinkToken`, `TelegramLoginRequest`, `TrafficSample` (см. [domain-model.md](domain-model.md)).
|
||||
- **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды). Это единственный VO —
|
||||
`TrafficLimit`/`ConnectionLink` из раннего плана не понадобились (лимиты трафика — backlog,
|
||||
connection string строит `IXuiPanelGateway` на лету).
|
||||
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`, `ActivationStatus`, `AuditSource`,
|
||||
`TelegramLoginStatus`, `OsPlatform`.
|
||||
- **Domain Exceptions**: `DomainException` — брошенный при нарушении инварианта в самой сущности
|
||||
(например, `Revoke()` уже отозванного конфига); хендлеры такие нарушения не ожидают в норме.
|
||||
- Инварианты и бизнес-правила инкапсулированы в сущностях (rich domain model: приватные сеттеры,
|
||||
фабричные методы, поведенческие методы), а не в хендлерах.
|
||||
|
||||
> `AppUser` (Identity) живёт в `Infrastructure` (зависит от `IdentityUser`), а домен ссылается
|
||||
> на пользователя по `UserId` (Guid), чтобы не тащить Identity в ядро.
|
||||
> `AppUser`/`AppRole` (Identity) живут в `Infrastructure` (наследуют `IdentityUser<Guid>`/
|
||||
> `IdentityRole<Guid>`), а домен ссылается на пользователя/роль только по `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>**: явная модель успеха/ошибки вместо исключений для управляемых сценариев.
|
||||
- **Commands / Queries** + их **Handlers** (`ICommandHandler<,>` / `IQueryHandler<,>` — свои интерфейсы),
|
||||
организованы по фичам (`Auth/Login/`, `Configs/Create/`, `Admin/Nodes/`, ...).
|
||||
- **Ports (интерфейсы)**: `IAppDbContext`, `IXuiPanelGateway`, `ICurrentUser`, `IIdentityService`,
|
||||
`ISecretProtector`, `IRealtimeNotifier`, `ITelegramNotifier`, `IRoleService`.
|
||||
- **Validators**: FluentValidation на команды, где есть что проверять помимо типов (не на все — см.
|
||||
[backend-conventions.md](backend-conventions.md)).
|
||||
- **DTO**: плоские `record`, конвертация из сущностей — статический метод `FromDomain(...)` на самом
|
||||
DTO. Mapster из исходного плана не пригодился — при таком числе полей ручной маппинг читается
|
||||
не хуже конфига маппера и не создаёт лишней зависимости.
|
||||
- **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция +
|
||||
`SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` нет — авторизация (роль,
|
||||
активация) — это либо `RequireAuthorization()`/`RequireRole(...)` на эндпоинте, либо явная проверка
|
||||
в начале хендлера (например, «инбаунд доступен роли пользователя»).
|
||||
- **Result<T>**: явная модель успеха/ошибки (`Result`/`Result<T>`, `Error` с `ErrorType`) вместо
|
||||
исключений для управляемых сценариев.
|
||||
|
||||
### 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).
|
||||
`IEntityTypeConfiguration<T>` для маппингов; миграции EF Core. Репозиториев нет — хендлеры работают
|
||||
через `IAppDbContext` напрямую (`DbSet<T>` + LINQ).
|
||||
- **Identity & Auth**: ASP.NET Core Identity, `JwtTokenService` (access + refresh), `RefreshTokenService`
|
||||
(хранение/ротация/отзыв refresh-токенов), `RoleService`, `DbInitializer` (сидинг).
|
||||
- **3x-ui интеграция**: `XuiPanelGateway : IXuiPanelGateway` поверх `ThreeXui.Net`; кэш клиентов per-node
|
||||
внутри самого гейтвея (см. ниже — отдельного класса-фабрики нет).
|
||||
- **Background jobs**: `TrafficSyncService`, `NodeHealthCheckService`, `TrafficRetentionService`
|
||||
(`BackgroundService` + `PeriodicTimer`).
|
||||
- **Secrets**: `DataProtectionSecretProtector : ISecretProtector` (шифрование паролей нод at-rest,
|
||||
ASP.NET Core Data Protection, key-ring на томе `dp_keys`).
|
||||
- **Telegram**: `TelegramNotifier : ITelegramNotifier` — отправка DM-уведомлений через `ITelegramBotClient`.
|
||||
|
||||
> **SignalR-пуш физически лежит в `PnvPanel.Api/Hubs/`, не в `Infrastructure`.**
|
||||
> `SignalRRealtimeNotifier : IRealtimeNotifier` нужен `IHubContext<PanelHub>`, а сам `PanelHub`
|
||||
> определён там же — не было смысла тащить эту связку через слой. `Application` всё равно видит
|
||||
> только порт `IRealtimeNotifier`, так что граница зависимостей не нарушена.
|
||||
|
||||
### 4. `PnvPanel.Api` (Presentation)
|
||||
Композиционный корень и транспорт.
|
||||
|
||||
- **Minimal API** эндпоинты, сгруппированные по фичам (`MapAuthEndpoints`, `MapConfigEndpoints`, `MapAdminEndpoints`).
|
||||
- **SignalR Hubs**: `PanelHub`.
|
||||
- **Telegram-бот**: `TelegramBotHostedService` + хендлеры апдейтов в `Telegram/` (см. отдельный раздел).
|
||||
- **Minimal API**-эндпоинты, сгруппированные по фичам — 12 файлов в `Endpoints/`
|
||||
(`AuthEndpoints`, `ActivationEndpoints`, `ConfigEndpoints`, `AppEndpoints`, `SubscriptionEndpoints`,
|
||||
`TelegramEndpoints`, `AdminUserEndpoints`, `AdminAppEndpoints`, `AdminStatsEndpoints`, `NodeEndpoints`,
|
||||
`InboundEndpoints`, `RoleEndpoints`); полный список маршрутов — [api-design.md](api-design.md).
|
||||
Каждый эндпоинт аннотирован `.Produces<T>()`, чтобы OpenAPI-схема полностью описывала тело ответа
|
||||
(нужно для `pnpm gen:api` на фронте).
|
||||
- **SignalR Hubs**: `PanelHub` (`Hubs/`).
|
||||
- **Telegram-бот**: `TelegramBotHostedService` + `PnvBotUpdateHandler` в `Telegram/` (см. отдельный раздел).
|
||||
- **Статика SPA**: раздача собранного фронта из `wwwroot` + SPA-fallback (единый контейнер).
|
||||
- **Middleware**: обработка исключений → ProblemDetails, корреляция запросов, rate limiting.
|
||||
- **DI**: `AddApplication()`, `AddInfrastructure()`, `AddApiServices()` — сборка всех слоёв.
|
||||
- **OpenAPI**: Swashbuckle + Scalar UI; генерация схемы для codegen фронта.
|
||||
- **Ошибки**: встроенные `AddProblemDetails()` + `UseExceptionHandler()` (ASP.NET Core, без кастомного
|
||||
middleware) конвертируют необработанные исключения в `application/problem+json`.
|
||||
- **DI**: `AddApplication()` (Application), `AddInfrastructure()` (Infrastructure) + прямая регистрация
|
||||
в `Program.cs` для того, что специфично Api-слою (SignalR, Telegram-клиент, rate limiting).
|
||||
- **OpenAPI**: нативный `Microsoft.AspNetCore.OpenApi` (`/openapi/v1.json`) + `Scalar.AspNetCore`
|
||||
UI (`/scalar`) — без Swashbuckle.
|
||||
|
||||
## CQRS
|
||||
|
||||
@@ -97,36 +130,44 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
|
||||
через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — `ICommand<T>`,
|
||||
`IQuery<T>`, `ICommandHandler<,>`, `IQueryHandler<,>`, `IPipelineBehavior<,>`.
|
||||
|
||||
Пример потока «создать конфиг»:
|
||||
Пример потока «создать конфиг» (`backend/src/PnvPanel.Application/Configs/Create/CreateVpnConfigCommandHandler.cs`):
|
||||
```
|
||||
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 }
|
||||
→ ValidationBehavior (FluentValidation — формат inboundId/label/deviceLimit)
|
||||
→ CreateVpnConfigCommandHandler
|
||||
· проверяет активацию + роль инбаунда (доменные проверки)
|
||||
· SELECT pg_advisory_xact_lock(hashtext(userId)) — сериализует параллельные создания
|
||||
· пересчитывает текущее число активных конфигов и сверяет с AppRole.MaxConfigs
|
||||
· IXuiPanelGateway.AddClientAsync(node, inbound, ...) // 3x-ui, получает ClientExternalId
|
||||
· VpnConfig.Create(...) + AssignRemoteClient(id), сохраняет через IAppDbContext
|
||||
· при сбое SaveChanges после успешного AddClientAsync — компенсация (RemoveClientAsync)
|
||||
→ UnitOfWorkBehavior (commit транзакции)
|
||||
→ 200 OK VpnConfigDto { id, label, protocol, location, deviceLimit, usedUpBytes, usedDownBytes,
|
||||
expiresAt, status, createdAt }
|
||||
```
|
||||
Ссылка подключения в ответ создания **не входит** — фронт запрашивает её отдельно,
|
||||
`GET /api/configs/{id}/link`, по кнопке на карточке конфига (см. [api-design.md](api-design.md)).
|
||||
|
||||
## Интеграция с 3x-ui (ThreeXui.Net)
|
||||
|
||||
`ThreeXui.Net` конфигурируется на **один** `BaseAddress`, а у нас **несколько нод**. Поэтому:
|
||||
|
||||
- Порт `IXuiPanelGateway` инкапсулирует все операции с панелями и принимает `Node` (или его id):
|
||||
- Порт `IXuiPanelGateway` инкапсулирует все операции с панелями и принимает `Node`:
|
||||
`ListInboundsAsync`, `AddClientAsync`, `UpdateClientAsync`, `RemoveClientAsync`,
|
||||
`GetClientTrafficAsync`, `BuildConnectionStringAsync`, `ProbeAsync`.
|
||||
- `XuiPanelGateway` держит **фабрику/кэш `IXuiClient` per-node** (ключ — `NodeId`), создавая клиента
|
||||
из расшифрованных `NodeCredentials` через `XuiHttpClientFactory`/`HttpClient`. Cookie-session и
|
||||
авто-переавторизация на 401 обеспечиваются самой библиотекой.
|
||||
`GetClientTrafficAsync`, `BuildConnectionStringAsync`, `ProbeAsync`, `ValidateBaseAddress`,
|
||||
`InvalidateClient(nodeId)` (вызывается после смены креденшлов ноды).
|
||||
- `XuiPanelGateway` — единственная реализация, держит `ConcurrentDictionary<Guid, Lazy<IXuiClient>>`
|
||||
(ключ — `NodeId`), создавая клиента из расшифрованных `NodeCredentials` лениво при первом обращении
|
||||
к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`.
|
||||
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
|
||||
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
|
||||
- **Реконсиляция дрейфа**: 3x-ui — источник правды по клиентам. При синхронизации сверяем проекцию
|
||||
с панелью: клиент удалён/изменён напрямую в 3x-ui → помечаем конфиг рассинхронизованным
|
||||
(`Disabled`/флаг) и логируем; не «воскрешаем» молча. Наши записи о трафике/статусах обновляем из панели.
|
||||
- **Дрейф с 3x-ui в MVP не реконсилируется активно**: `TrafficSyncService` при недоступной ноде или
|
||||
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
|
||||
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
|
||||
в 3x-ui в обход панели, локальная запись `VpnConfig` продолжит существовать до следующего
|
||||
явного действия пользователя/админа (`Revoke`/`Rotate`), которое обнаружит несоответствие по ответу
|
||||
гейтвея. Активная сверка/алертинг по дрейфу — задел на будущее, не реализовано.
|
||||
|
||||
## Telegram-бот (presentation-адаптер)
|
||||
|
||||
@@ -134,7 +175,9 @@ POST /api/configs
|
||||
бизнес-правил). Полное описание — в [telegram-bot.md](telegram-bot.md). Ключевое для архитектуры:
|
||||
|
||||
- Хостится **в процессе Api** как `BackgroundService` (`TelegramBotHostedService`) — это условие
|
||||
для упаковки «фронт+бек в одном контейнере». Транспорт — **long polling** (MVP), webhook — опция.
|
||||
для упаковки «фронт+бек в одном контейнере». Транспорт — только **long polling**
|
||||
(`ITelegramBotClient.ReceiveAsync`); webhook рассматривался, но не реализован — конфигурации
|
||||
`Telegram:Mode`/`WebhookUrl` в коде нет.
|
||||
- Обращения к домену — только через собственный `ISender`, теми же командами/запросами, что и веб
|
||||
(`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...). `Telegram.Bot`
|
||||
не проникает в Application/Domain.
|
||||
@@ -144,21 +187,25 @@ POST /api/configs
|
||||
## Realtime (SignalR)
|
||||
|
||||
- Хаб `PanelHub` (`/hubs/panel`), авторизация по тому же JWT.
|
||||
- **Группы**: `user:{userId}` (личные события), `admins` (события нод/системы).
|
||||
- **События сервер→клиент** (см. [api-design.md](api-design.md)): `configTrafficUpdated`,
|
||||
`configStatusChanged`, `nodeStatusChanged`.
|
||||
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`), вызываемый из хендлеров и
|
||||
фоновых сервисов — Application-слой не зависит от SignalR напрямую.
|
||||
- **Группы** (`GroupNames` в `Api/Hubs/PanelHub.cs`): `user:{userId}` (личные события), `admins`
|
||||
(события нод/системы/активации) — пользователь при подключении добавляется в свою `user:{userId}`
|
||||
и, если он админ, дополнительно в `admins`.
|
||||
- **События сервер→клиент**: `configTrafficUpdated`, `configStatusChanged`, `nodeStatusChanged`,
|
||||
`activationRequested`, `userActivated` — точные payload'ы см. [api-design.md](api-design.md#signalr--hub-hubspanel).
|
||||
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`, реализация в `Api/Hubs/`),
|
||||
вызываемый из хендлеров и фоновых сервисов — Application-слой не зависит от SignalR напрямую.
|
||||
|
||||
## Фоновые задачи
|
||||
|
||||
- **TrafficSyncService** — периодически (`PeriodicTimer`) обходит активные ноды, тянет трафик по
|
||||
клиентам, обновляет `VpnConfig`, пишет `TrafficSample` (для графиков), шлёт realtime-события,
|
||||
помечает превышения (`TrafficLimitReached`).
|
||||
- **NodeHealthCheckService** — health-probe нод, обновляет `NodeStatus`, оповещает `admins`.
|
||||
клиентам через `IXuiPanelGateway.GetClientTrafficAsync`, пишет `VpnConfig.UpdateTraffic(...)` и
|
||||
`TrafficSample`, шлёт `configTrafficUpdated`. Трафик используется только для отображения — лимиты
|
||||
и автоотключение по превышению не реализованы (см. [domain-model.md](domain-model.md)).
|
||||
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
|
||||
шлёт `nodeStatusChanged` группе `admins`.
|
||||
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
|
||||
- Для MVP — встроенный `BackgroundService`; при росте нагрузки — вынести в Hangfire/Quartz
|
||||
(см. [tech-stack.md](tech-stack.md)).
|
||||
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера — для
|
||||
нагрузки MVP этого достаточно (см. [tech-stack.md](tech-stack.md)).
|
||||
|
||||
## Сидирование и старт
|
||||
|
||||
@@ -167,15 +214,19 @@ POST /api/configs
|
||||
|
||||
- **Системные роли**: `admin` (без лимита конфигов) и `user` (`MaxConfigs = Roles__DefaultUserMaxConfigs`, по умолчанию 3).
|
||||
- **Учётка администратора**: создаётся из `AdminSeed__Username` / `AdminSeed__Password`, если ещё нет;
|
||||
сразу активирована и с ролью `admin`.
|
||||
- **Telegram id админов** (`AdminSeed__TelegramUserIds`) — авторизуют админ-действия в боте и
|
||||
адресуют уведомления (например, запросы на активацию).
|
||||
сразу активирована и с ролью `admin`. Seed-админ **не привязывается к Telegram автоматически** —
|
||||
привязка делается вручную в UI, как у любого пользователя.
|
||||
- **Каталог приложений** (`ClientApp`): если таблица пуста — сидируется из
|
||||
[`seed/client-apps.json`](../seed/client-apps.json) (стартовый набор клиентов по ОС). Дальше — правки через админ-CRUD.
|
||||
|
||||
Сидинг не перезаписывает существующие данные. Принудительной смены сид-пароля при первом входе
|
||||
**нет** — задавайте сильный `AdminSeed__Password` сразу; сменить пароль можно в приложении.
|
||||
|
||||
Отдельно от сидинга — **Telegram id админов** (`Telegram__AdminTelegramUserIds`, через запятую)
|
||||
читаются `TelegramOptions` **напрямую при каждой проверке** (не пишутся в БД): именно этот список
|
||||
авторизует нажатие «Активировать/Отклонить» в боте и определяет, кому слать уведомления о новых
|
||||
запросах активации.
|
||||
|
||||
## RBAC — динамические роли и активация
|
||||
|
||||
- `AppRole` расширяет `IdentityRole<Guid>` полем `MaxConfigs`. **У пользователя ровно одна роль**;
|
||||
@@ -189,34 +240,50 @@ POST /api/configs
|
||||
сохраняются, создание новых блокируется до входа в квоту.
|
||||
- **Блокировка пользователя**: `IsBlocked = true` → вход запрещён + все конфиги `Disabled` (отключение
|
||||
клиентов в 3x-ui); разблокировка — обратная операция. Пишется в `AuditLog`.
|
||||
- Уведомления: запросы активации → группа `admins` (SignalR) + Telegram (по `AdminSeed__TelegramUserIds`);
|
||||
решения/блокировки → пользователю (SignalR + Telegram-DM, если привязан).
|
||||
- Уведомления: запросы активации → группа `admins` (SignalR) + Telegram (по
|
||||
`Telegram__AdminTelegramUserIds`); решения/блокировки → пользователю (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`).
|
||||
- **Восстановление пароля**: через привязанный Telegram (passwordless-вход → смена пароля); без
|
||||
привязки — сброс админом (`ResetUserPasswordCommand`). Email/SMTP не используются. Смена пароля
|
||||
вошедшим — `POST /api/auth/change-password`.
|
||||
- **AuthZ**: именованных policy нет — либо `.RequireAuthorization()` (любой вошедший) или
|
||||
`.RequireAuthorization(policy => policy.RequireRole(RoleNames.Admin))` на группе эндпоинтов, либо
|
||||
явная проверка внутри хендлера (владение конфигом — сравнение `VpnConfig.UserId` с `ICurrentUser`;
|
||||
активация — `ConfigErrors.NotActivated`).
|
||||
- **Секреты нод**: шифруются `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).
|
||||
- **CSRF**: явного анти-CSRF токена нет — все мутации API читают авторизацию только из
|
||||
`Authorization: Bearer` (JS должен явно прочитать access-token из памяти и подставить заголовок,
|
||||
чужой сайт этого сделать не может). Refresh-cookie (`pnv_refresh_token`) — единственное, что браузер
|
||||
шлёт автоматически; она `HttpOnly`, `SameSite=Strict`, `Path=/api/auth`, и `Secure` выставляется по
|
||||
`HttpContext.Request.IsHttps` (учитывает `ForwardedHeaders` за прокси) — этого достаточно, т.к. сама
|
||||
по себе она не даёт мутировать данные, только обменивается на access-token эндпоинтом `/api/auth/refresh`.
|
||||
- **Brute-force**: Identity lockout по числу неудачных входов; rate-limit на `/auth/*` (настраиваемый
|
||||
лимит, `RateLimiting:AuthPermitLimit`, по умолчанию 20 запросов/мин).
|
||||
- **Rate limiting**: встроенный `RateLimiter` .NET, один fixed-window лимит (`RateLimiting:AuthPermitLimit`,
|
||||
по умолчанию 20/мин) применён к `/api/auth/*`, `/api/auth/telegram/*` и `/sub/{token}`; остальные
|
||||
эндпоинты (в т.ч. создание конфигов) им не покрыты.
|
||||
- **Валидация входа**: FluentValidation + жёсткая типизация DTO; ошибки — единый `ProblemDetails`.
|
||||
- **CORS**: в проде фронт и бек — один origin (CORS не нужен); в dev — строгий allowlist (`App__CorsOrigins`).
|
||||
- **CORS**: не настроен вообще (`AddCors`/`UseCors` в коде нет) — фронт и бек всегда один origin: в
|
||||
проде раздаются из одного образа, в dev Vite проксирует `/api`/`/hubs`, так что браузер никогда не
|
||||
делает кросс-origin запрос. Отдельного allowlist-конфига для CORS сейчас не существует.
|
||||
- **Аудит**: значимые действия (активация, блокировка, смена роли, отзыв, ноды/инбаунды) пишутся в
|
||||
`AuditLog` (append-only) с источником `Web`/`Telegram`/`System`.
|
||||
|
||||
## Обработка ошибок
|
||||
|
||||
- Управляемые ошибки → `Result`/`Result<T>` → маппинг в HTTP-статус + `ProblemDetails`.
|
||||
- Непредвиденные исключения → глобальный middleware → 500 + корреляция + структурный лог (без утечки деталей).
|
||||
- Доменные исключения (нарушение инвариантов) → 409/422 с понятным сообщением.
|
||||
- Управляемые ошибки → `Result`/`Result<T>` (`ResultExtensions.ToHttpResult`) → маппинг `ErrorType` в
|
||||
HTTP-статус + `application/problem+json` (400/401/403/404/409/422).
|
||||
- Непредвиденные исключения → встроенные `AddProblemDetails()` + `UseExceptionHandler()` → 500 без
|
||||
утечки деталей + `Serilog` request-логирование (`UseSerilogRequestLogging`, обогащение —
|
||||
`Enrich.FromLogContext()`). Сквозного `CorrelationId`/явного обогащения `UserId`/`NodeId`/`ConfigId`
|
||||
в логах пока нет — задел на будущее, а не то, на что стоит полагаться при расследовании инцидентов сегодня.
|
||||
- Доменные исключения (нарушение инвариантов) — `DomainException`, ожидаются только как баг, а не
|
||||
штатный путь (штатные отказы — через `Result.Failure`, не исключения).
|
||||
|
||||
## Развёртывание (единый контейнер приложения)
|
||||
|
||||
@@ -238,7 +305,7 @@ PostgreSQL:
|
||||
- **Миграции**: применяются **автоматически на старте** приложения (в MVP; при масштабировании на
|
||||
несколько инстансов — вынести в отдельный шаг/джобу).
|
||||
- Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения,
|
||||
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`, `PublicSiteUrl`).
|
||||
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`).
|
||||
|
||||
```
|
||||
[ внешний прокси/шлюз: TLS termination ] ← HTTPS, вне нашего compose
|
||||
|
||||
Reference in New Issue
Block a user