Refactor environment configuration and update documentation for MVP status
CI / Backend (build + test) (push) Successful in 1m15s
CI / Frontend (lint + typecheck + build) (push) Successful in 30s

- 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:
Leonid Pershin
2026-07-02 14:12:50 +03:00
parent 7e8435ee76
commit cdd67f8e2b
14 changed files with 896 additions and 616 deletions
+110 -58
View File
@@ -2,52 +2,74 @@
## Структура решения
Solution-файл — **`PnvPanel.slnx`** (новый XML-формат dotnet CLI, не классический `.sln`).
```
backend/
PnvPanel.sln
PnvPanel.slnx
src/
PnvPanel.Domain/
Common/ # Entity, AggregateRoot, IDomainEvent, ValueObject base
Nodes/ # Node, NodeCredentials, NodeStatus, события
Inbounds/ # Inbound, VpnProtocol
Configs/ # VpnConfig, ConfigStatus, TrafficLimit, события
Plans/ # Plan
Exceptions/ # DomainException и наследники
Common/ # Entity (единственный базовый класс — без AggregateRoot/IDomainEvent)
Activation/ # ActivationRequest, ActivationStatus
Apps/ # ClientApp, OsPlatform
Audit/ # AuditLog, AuditSource
Configs/ # VpnConfig, ConfigStatus, TrafficSample
Inbounds/ # Inbound, VpnProtocol
Nodes/ # Node, NodeCredentials (VO), NodeStatus
Telegram/ # TelegramLinkToken, TelegramLoginRequest, TelegramLoginStatus
Exceptions/ # DomainException
PnvPanel.Application/
Common/
Behaviors/ # Validation, Logging, UnitOfWork, Authorization
Interfaces/ # IAppDbContext, IXuiPanelGateway, ICurrentUser, IRealtimeNotifier, ...
Behaviors/ # ValidationBehavior, LoggingBehavior, UnitOfWorkBehavior (нет Authorization-поведения)
Interfaces/ # IAppDbContext, IXuiPanelGateway, ICurrentUser, IIdentityService,
# ISecretProtector, IRealtimeNotifier, ITelegramNotifier, IRoleService
Messaging/ # ISender, ICommand<T>, IQuery<T>, ICommandHandler<,>, IQueryHandler<,>, IPipelineBehavior<,>
Models/ # Result<T>, Error, PagedList<T>
Mapping/ # Mapster-конфиги
Auth/ # Register/Login/Refresh (Commands, Handlers, Validators, DTOs)
Configs/ # CreateVpnConfig, EditVpnConfig, RotateVpnConfig, RevokeVpnConfig, GetMyConfigs, GetConfigLink, GetSubscription, ...
Nodes/ # RegisterNode, SyncNode, ListNodes, ...
Inbounds/ # PublishInbound, ListInbounds, ...
Apps/ # (admin) CRUD каталога ClientApp; GetApps (по ОС) для юзера
Admin/ # ListUsers, BlockUser/UnblockUser, ChangeUserRole, GetStats, Audit, ...
Models/ # Result, Result<T>, Error, PagedList<T>, RoleQuota
Activation/ # RequestActivationCommand, GetActivationStatusQuery (пользовательские)
Admin/
Activation/ # ListActivationRequestsQuery, Approve/RejectActivationCommand
Apps/ # CRUD ClientApp
Audit/ # ListAuditLogsQuery
Inbounds/ # ListInbounds, PublishInbound
Nodes/ # RegisterNode, UpdateNode, DeleteNode, SyncNode, ProbeNode, ListNodes
Roles/ # ListRoles, CreateRole, UpdateRole, DeleteRole
Stats/ # GetStatsQuery
Users/ # ListUsers, BlockUser/UnblockUser, ChangeUserRole, ResetUserPassword,
# ForceRevokeConfig, GetUserConfigs
Apps/ # ListAppsQuery (по ОС, для юзера)
Auth/
ChangePassword/, DeleteMyAccount/, Login/, Logout/, Me/, Refresh/, Register/
Configs/
Create/, Edit/, Rotate/, Revoke/, GetMyConfigs/, GetConfigLink/, GetMySubscription/,
ListAvailableInbounds/
Subscriptions/ # SubscriptionDto, GetUserSubscriptionQuery, GetConfigSubscriptionQuery
Telegram/ # LinkTelegramCommand, CreateLinkTokenCommand, CreateLoginRequestCommand,
# Approve/RejectTelegramLoginCommand, GetLoginRequestStatusQuery, ...
# Telegram/Bot/ — контракт для бота (не сам Telegram.Bot)
PnvPanel.Infrastructure/
Persistence/
AppDbContext.cs
AppDbContext.cs # : IdentityDbContext<AppUser, AppRole, Guid>, IAppDbContext
Configurations/ # IEntityTypeConfiguration<T>
Migrations/
Identity/ # AppUser, AppRole, JwtTokenService, RefreshToken
Xui/ # XuiPanelGateway, XuiClientFactory (per-node)
Realtime/ # SignalRRealtimeNotifier
BackgroundJobs/ # TrafficSyncService, NodeHealthCheckService
Identity/ # AppUser, AppRole, JwtTokenService, RefreshTokenService, RoleService,
# DbInitializer (сидинг), IdentityService, CurrentUser
Xui/ # XuiPanelGateway (единственный файл — кэш клиентов per-node внутри него)
BackgroundJobs/ # TrafficSyncService, NodeHealthCheckService, TrafficRetentionService
Security/ # DataProtectionSecretProtector
DependencyInjection.cs
Telegram/ # TelegramNotifier, TelegramOptions
DependencyInjection.cs # AddInfrastructure(...)
PnvPanel.Api/
Endpoints/ # AuthEndpoints, ConfigEndpoints, NodeEndpoints, AdminEndpoints, SubscriptionEndpoints
Hubs/ # PanelHub
Middleware/ # ExceptionHandling, RequestCorrelation
Extensions/ # AddApiServices, UseApiPipeline
Program.cs
Endpoints/ # 12 файлов, см. backend-conventions.md ниже и api-design.md
Hubs/ # PanelHub, SignalRRealtimeNotifier (реализация IRealtimeNotifier — здесь,
# не в Infrastructure, т.к. нужен IHubContext<PanelHub>)
Telegram/ # TelegramBotHostedService, PnvBotUpdateHandler, TelegramNotifier
Common/ # RateLimiting (константы политик), ResultExtensions (Result -> IResult)
Program.cs # DI composition root, pipeline (нет отдельных Middleware/Extensions папок)
appsettings*.json
tests/
PnvPanel.Domain.Tests/
PnvPanel.Application.Tests/
PnvPanel.Integration.Tests/ # Testcontainers PostgreSQL
PnvPanel.Domain.Tests/ # 54 теста
PnvPanel.Application.Tests/ # 71 тест
PnvPanel.IntegrationTests/ # 9 тестов, Testcontainers.PostgreSql + WebApplicationFactory<Program>
```
Организация Application — **по фичам** (feature folders), внутри слоёв Clean Architecture.
@@ -57,27 +79,43 @@ backend/
- Классы/методы/свойства — `PascalCase`; параметры/локальные — `camelCase`; приватные поля — `_camelCase`.
- Команды — `<Verb><Noun>Command` (`CreateVpnConfigCommand`), запросы — `<Get/List><Noun>Query`.
- Хендлеры — `<Command/Query>Handler`; валидаторы — `<Command/Query>Validator`.
- DTO — суффикс `Dto` (`VpnConfigDto`); ответы эндпоинтов — `Response`, тела запросов — `Request`.
- **DTO** (Application-слой, возвращаются из `Result<T>`) — суффикс `Dto` (`VpnConfigDto`, `NodeDto`);
конвертация из сущности — статический `FromDomain(entity, ...)` на самом DTO.
- **Тела запросов** (Api-слой, только для JSON-полей, которых нет в готовой команде) — суффикс `Body`
(`CreateConfigBody`, `UpdateNodeBody`) либо сам record команды биндится напрямую как тело
(`RegisterCommand`, `LoginCommand`).
- **Тела ответов, которых нет как Application DTO** (например, потому что Api-слой добавляет
вычисляемое поле — абсолютный URL из токена) — суффикс `ResponseDto` (`AuthResponseDto`,
`ConfigLinkResponseDto`, `TelegramLoginStatusResponseDto`), определяются прямо в файле эндпоинта.
- Async-методы — суффикс `Async`, всегда принимают `CancellationToken`.
- Один публичный тип на файл; имя файла = имя типа.
- Один публичный тип на файл — с исключением: Api-слой держит вспомогательные `Body`/`ResponseDto`
records в том же файле, что и класс эндпоинтов, который их использует (не выносятся отдельно).
## Паттерны
- **Rich domain model**: инварианты в сущностях (приватные сеттеры, фабричные методы `Node.Create(...)`,
- **Rich domain model**: инварианты в сущностях (приватные сеттеры, фабричные методы `Node.Register(...)`,
поведенческие методы `config.Revoke()`), а не анемичные DTO-сущности.
- **CQRS через собственный диспетчер**: хендлеры реализуют `ICommandHandler<TCommand,TResult>` /
`IQueryHandler<,>`; `ISender` резолвит их из DI и прогоняет через `IPipelineBehavior<,>`
(валидация, транзакция, логирование). Без внешних CQRS-библиотек.
- **Порты в Application, адаптеры в Infrastructure**: никакого `Npgsql`/`SignalR`/`ThreeXui.Net` в Application/Domain.
- **`IAppDbContext`** экспонирует `DbSet<>` и `SaveChangesAsync`; хендлеры пишут LINQ напрямую
(репозитории — только для сложной агрегатной логики).
- **Result-модель**: команды/запросы возвращают `Result<T>`; эндпоинт маппит в HTTP (`.Match(...)`).
- **Транзакция на команду**: `UnitOfWorkBehavior` оборачивает выполнение команды в транзакцию.
- **Валидация**: `ValidationBehavior` до хендлера; хендлер не проверяет формат ввода повторно.
- **Идемпотентность**: команды к 3x-ui устойчивы к повторам; при частичном сбое — компенсация
(создали клиента в панели, но упала БД → удалить клиента, вернуть ошибку).
- **Оптимистичная блокировка**: на изменяемых сущностях (нода, конфиг) — `xmin`/rowversion, чтобы
параллельные правки не затирали друг друга.
(`ValidationBehavior``LoggingBehavior``UnitOfWorkBehavior`). Без внешних CQRS-библиотек, без
доменных событий — хендлер сам вызывает нужные порты (realtime/Telegram/аудит) синхронно.
- **Порты в Application, адаптеры в Infrastructure/Api**: никакого `Npgsql`/`SignalR`/`ThreeXui.Net` в
Application/Domain — только интерфейсы, реализации могут жить и в `Infrastructure`, и в `Api`
(`IRealtimeNotifier` реализован в `Api/Hubs`, т.к. завязан на `IHubContext<PanelHub>`).
- **`IAppDbContext`** экспонирует `DbSet<>` и `SaveChangesAsync`; хендлеры пишут LINQ напрямую —
выделенных репозиториев нет вообще.
- **Result-модель**: команды/запросы возвращают `Result`/`Result<T>`; `ResultExtensions.ToHttpResult()`
мапит `Error.Type` в HTTP-статус на границе Api.
- **Транзакция на команду**: `UnitOfWorkBehavior` вызывает `SaveChangesAsync` после хендлера команды
(не запросов) — отдельной BEGIN/COMMIT-транзакции вокруг этого в MVP нет, полагаемся на то, что
один `SaveChanges` уже атомарен для одной единицы работы.
- **Компенсация при частичном сбое**: если клиент успешно создан в 3x-ui, а `SaveChanges` в БД упал —
хендлер вызывает `RemoveClientAsync`, чтобы не оставить сироту в панели.
- **Защита от гонок на квоте — `pg_advisory_xact_lock`**, не оптимистичная блокировка: перед проверкой
квоты роли `CreateVpnConfigCommandHandler` берёт `pg_advisory_xact_lock(hashtext(userId))`
сериализует параллельные попытки создать конфиг одним и тем же пользователем в рамках транзакции.
Отдельного rowversion/`xmin` на `Node`/`VpnConfig` нет (единственный `IsConcurrencyToken` в схеме —
штатный `ConcurrencyStamp` таблиц Identity).
## Работа с 3x-ui
@@ -92,25 +130,39 @@ backend/
## Ошибки и логирование
- Единый `ProblemDetails` для ошибок API; коды: 400 (валидация), 401/403 (auth), 404, 409 (конфликт домена), 422, 429 (rate limit), 500.
- Serilog со структурными полями (`UserId`, `NodeId`, `ConfigId`, `CorrelationId`); секреты не логировать.
- Единый `application/problem+json` для ошибок API (`ResultExtensions.ToProblem`); коды: 400 (валидация),
401/403 (auth/не активирован), 404, 409 (конфликт домена — квота, дубликат), 422 (прочее), 429 (rate limit), 500.
- Serilog + `UseSerilogRequestLogging()`; секреты (пароли, JWT, `BotToken`) не логировать. Структурного
обогащения `UserId`/`NodeId`/`ConfigId`/`CorrelationId` пока нет — см. [tech-stack.md](tech-stack.md).
## Тестирование
- **Domain.Tests** — инварианты и поведение сущностей, без моков.
- **Application.Tests** — хендлеры с подменёнными портами (NSubstitute), проверка веток `Result`.
- **Integration.Tests** — реальный PostgreSQL (Testcontainers), миграции, сквозные сценарии эндпоинтов;
3x-ui — мок гейтвея или фейковый HTTP-сервер.
- Именование тестов: `Method_Scenario_ExpectedResult`.
- **PnvPanel.Domain.Tests** (54 теста) — инварианты и поведение сущностей, без моков.
- **PnvPanel.Application.Tests** (71 тест) — хендлеры на EF Core InMemory + подменённые порты
(NSubstitute), проверка веток `Result`. InMemory, а не Sqlite — модель использует Postgres-специфичные
типы (`uuid[]`, `jsonb`), которые Sqlite не поддерживает, а InMemory просто игнорирует.
- **PnvPanel.IntegrationTests** (9 тестов) — реальный PostgreSQL (`Testcontainers.PostgreSql`) +
`WebApplicationFactory<Program>`, сквозные сценарии через реальный HTTP-контракт, включая проверку
`pg_advisory_xact_lock` под параллельной нагрузкой на квоту; 3x-ui подменён `FakeXuiPanelGateway`.
- Именование тестов: `Method_Scenario_ExpectedResult`. Обычные `Assert.*` из xUnit — без FluentAssertions.
## Конфигурация
- `appsettings.json` + `appsettings.{Environment}.json` + env vars (перекрывают).
- Секреты (JWT-ключ, строка подключения, ключ шифрования) — user-secrets (dev) / env/secret-store (prod).
- Строго типизированные `IOptions<T>` для секций конфига; валидация опций на старте.
- `appsettings.json` + `appsettings.{Environment}.json` + env vars (перекрывают); в dev — `.env`
через `docker-compose`'s `env_file`, локально без Docker — переменные окружения напрямую (проект
не подключает `dotnet user-secrets``UserSecretsId` в `.csproj` нет).
- Секреты (JWT-ключ, строка подключения, `Telegram:BotToken`, пароль сид-админа) — только через env/secret-store.
- Строго типизированные `IOptions<T>` для секций конфига (`JwtOptions`, `AdminSeedOptions`,
`TelegramOptions`, `RolesOptions`, ...); явной валидации на старте (`ValidateOnStart`/data annotations)
нет — отсутствующий обязательный секрет обнаружится при первом обращении (например, `AddInfrastructure`
бросит `InvalidOperationException`, если не задан `ConnectionStrings:Default`), не раньше.
## Стиль и качество кода
- `.editorconfig` + анализаторы (`Microsoft.CodeAnalysis.NetAnalyzers`), nullable reference types **включены**.
- `dotnet format` в CI; предупреждения как ошибки для наших проектов.
- Комментарии — по необходимости (почему, а не что); публичные контракты портов документируем XML-doc.
- `.editorconfig`, nullable reference types **включены**; сборка в CI идёт с `-c Release` и должна
быть без предупреждений.
- `dotnet format` — локальная команда разработчика (см. корневой `CLAUDE.md`), **в CI не запускается**;
CI гоняет только `dotnet build`/`dotnet test` (backend) и `pnpm lint`/`typecheck`/`build` (frontend).
- Комментарии — по необходимости (почему, а не что, — см. примеры в коде: причина `pg_advisory_xact_lock`,
причина `Secure = request.IsHttps`); публичные контракты портов документируем XML-doc там, где это
не очевидно из имени.