- 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.
14 KiB
14 KiB
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
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
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 # : 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/ # 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/ # 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/ResponseDtorecords в том же файле, что и класс эндпоинтов, который их использует (не выносятся отдельно).
Паттерны
- Rich domain model: инварианты в сущностях (приватные сеттеры, фабричные методы
Node.Register(...), поведенческие методыconfig.Revoke()), а не анемичные DTO-сущности. - CQRS через собственный диспетчер: хендлеры реализуют
ICommandHandler<TCommand,TResult>/IQueryHandler<,>;ISenderрезолвит их из DI и прогоняет черезIPipelineBehavior<,>(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
- Только через порт
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.
Тестирование
- 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'senv_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 там, где это не очевидно из имени.