Files
PnvPanel/docs/backend-conventions.md
T
Leonid Pershin 9925968e22
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 36s
Update LiteCqrs integration and documentation
- Replaced references to local project connections with the NuGet package for LiteCqrs in multiple documentation files, ensuring clarity on dependency management.
- Updated architecture and backend conventions documentation to reflect the current state of the LiteCqrs library as a NuGet package, enhancing consistency across the project.
- Improved descriptions of CQRS implementation and command/query handling in the tech stack documentation, providing clearer guidance for developers.
2026-07-24 04:18:03 +03:00

175 lines
14 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.
# 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 — NuGet-пакет): хендлеры
реализуют `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 там, где это
не очевидно из имени.