- Replaced instances of the previous messaging system with LiteCqrs across various application components, enhancing the CQRS implementation. - Updated dependency injection to register LiteCqrs services and behaviors, streamlining command and query handling. - Adjusted multiple command and query handlers to align with the new messaging framework, ensuring consistent functionality and improved maintainability. - Added LiteCqrs package reference in the project file for better dependency management.
175 lines
14 KiB
Markdown
175 lines
14 KiB
Markdown
# Backend Conventions
|
||
|
||
## Структура решения
|
||
|
||
Solution-файл — **`PnvPanel.slnx`** (новый XML-формат dotnet CLI, не классический `.sln`).
|
||
|
||
```
|
||
backend/
|
||
PnvPanel.slnx
|
||
src/
|
||
PnvPanel.Domain/
|
||
Common/ # Entity (единственный базовый класс — без AggregateRoot/IDomainEvent)
|
||
Activation/ # ActivationRequest, ActivationStatus
|
||
Apps/ # ClientApp, OsPlatform
|
||
News/ # NewsPost
|
||
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/ # ValidationBehavior, LoggingBehavior, UnitOfWorkBehavior (нет Authorization-поведения)
|
||
Interfaces/ # IAppDbContext, IXuiPanelGateway, ICurrentUser, IIdentityService,
|
||
# ISecretProtector, IRealtimeNotifier, ITelegramNotifier, IRoleService
|
||
Messaging/ # ISender, ICommand<T>, IQuery<T>, ICommandHandler<,>, IQueryHandler<,>, IPipelineBehavior<,>
|
||
Models/ # Result, Result<T>, Error, PagedList<T>, RoleQuota
|
||
Activation/ # RequestActivationCommand, GetActivationStatusQuery (пользовательские)
|
||
Admin/
|
||
Activation/ # ListActivationRequestsQuery, Approve/RejectActivationCommand
|
||
Apps/ # CRUD ClientApp
|
||
News/ # CRUD NewsPost
|
||
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 (по ОС, для юзера)
|
||
News/ # ListNewsQuery (пагинировано, для юзера)
|
||
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 # : IdentityDbContext<AppUser, AppRole, Guid>, IAppDbContext
|
||
Configurations/ # IEntityTypeConfiguration<T>
|
||
Migrations/
|
||
Identity/ # AppUser, AppRole, JwtTokenService, RefreshTokenService, RoleService,
|
||
# DbInitializer (сидинг), IdentityService, CurrentUser
|
||
Xui/ # XuiPanelGateway (единственный файл — кэш клиентов per-node внутри него)
|
||
BackgroundJobs/ # TrafficSyncService, NodeHealthCheckService, TrafficRetentionService
|
||
Security/ # DataProtectionSecretProtector
|
||
Telegram/ # TelegramNotifier, TelegramOptions
|
||
DependencyInjection.cs # AddInfrastructure(...)
|
||
PnvPanel.Api/
|
||
Endpoints/ # 14 файлов, см. 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/ # 54 теста
|
||
PnvPanel.Application.Tests/ # 71 тест
|
||
PnvPanel.IntegrationTests/ # 9 тестов, Testcontainers.PostgreSql + WebApplicationFactory<Program>
|
||
```
|
||
|
||
Организация Application — **по фичам** (feature folders), внутри слоёв Clean Architecture.
|
||
|
||
## Именование
|
||
|
||
- Классы/методы/свойства — `PascalCase`; параметры/локальные — `camelCase`; приватные поля — `_camelCase`.
|
||
- Команды — `<Verb><Noun>Command` (`CreateVpnConfigCommand`), запросы — `<Get/List><Noun>Query`.
|
||
- Хендлеры — `<Command/Query>Handler`; валидаторы — `<Command/Query>Validator`.
|
||
- **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.Register(...)`,
|
||
поведенческие методы `config.Revoke()`), а не анемичные DTO-сущности.
|
||
- **CQRS через [LiteCqrs.Net](https://github.com/mrleo1nid/LiteCqrs.Net)** (собственная лёгкая
|
||
CQRS-библиотека, не MediatR — сосед-репозиторий, пока подключён `ProjectReference`'ом): хендлеры
|
||
реализуют `ICommandHandler<TCommand,TResult>` / `IQueryHandler<,>`; `ISender` резолвит их из DI и
|
||
прогоняет через `IPipelineBehavior<,>` (`LoggingBehavior` → `ValidationBehavior` →
|
||
`RequireActivationBehavior` → `UnitOfWorkBehavior`, порядок задан в `AddLiteCqrs(...)`). Без
|
||
доменных событий — хендлер сам вызывает нужные порты (realtime/Telegram/аудит) синхронно;
|
||
библиотека умеет Notifications/pub-sub, но PnvPanel их не использует.
|
||
- **Порты в 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-транзакции вокруг этого нет, полагаемся на то, что один
|
||
`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
|
||
|
||
- Только через порт `IXuiPanelGateway`. Гейтвей принимает `Node`/`NodeId` и разруливает per-node клиента.
|
||
- Пароли нод расшифровываются `ISecretProtector` **внутри** Infrastructure, никогда не покидают слой.
|
||
- Сетевые ошибки/недоступность → `Result.Failure`/статус ноды, не «пробрасываем» наружу как 500.
|
||
|
||
## Async / Cancellation
|
||
|
||
- Всё I/O — асинхронно; пробрасывать `CancellationToken` до EF Core и HTTP-вызовов.
|
||
- Не блокировать (`.Result`/`.Wait()`).
|
||
|
||
## Ошибки и логирование
|
||
|
||
- Единый `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).
|
||
|
||
## Тестирование
|
||
|
||
- **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 (перекрывают); в 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`, 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 там, где это
|
||
не очевидно из имени.
|