# 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, IQuery, ICommandHandler<,>, IQueryHandler<,>, IPipelineBehavior<,> Models/ # Result, Result, Error, PagedList, 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, IAppDbContext Configurations/ # IEntityTypeConfiguration 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) 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 ``` Организация Application — **по фичам** (feature folders), внутри слоёв Clean Architecture. ## Именование - Классы/методы/свойства — `PascalCase`; параметры/локальные — `camelCase`; приватные поля — `_camelCase`. - Команды — `Command` (`CreateVpnConfigCommand`), запросы — `Query`. - Хендлеры — `Handler`; валидаторы — `Validator`. - **DTO** (Application-слой, возвращаются из `Result`) — суффикс `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` / `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`). - **`IAppDbContext`** экспонирует `DbSet<>` и `SaveChangesAsync`; хендлеры пишут LINQ напрямую — выделенных репозиториев нет вообще. - **Result-модель**: команды/запросы возвращают `Result`/`Result`; `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`, сквозные сценарии через реальный 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` для секций конфига (`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 там, где это не очевидно из имени.