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
+64 -42
View File
@@ -1,22 +1,25 @@
# Domain Model
Домен — «rich model»: инварианты и переходы состояний живут в сущностях, а не в хендлерах.
`AppUser` — часть Identity (в `Infrastructure`); домен ссылается на пользователя по `UserId : Guid`.
`AppUser`/`AppRole` — часть Identity (живут в `Infrastructure`, т.к. расширяют `IdentityUser<Guid>`/
`IdentityRole<Guid>`); чистый `PnvPanel.Domain` ссылается на пользователя/роль только по `Guid`.
Ниже — то, что реально реализовано и работает. Тарифы `Plan` и лимиты трафика на конфиг
(`TrafficLimit`) были в первоначальном плане, но остались в backlog — квота в MVP только одна:
число активных конфигов на роль (`AppRole.MaxConfigs`).
## Диаграмма связей
```
AppUser (Identity) [+ IsActivated, TelegramUserId]
AppUser (Identity) [+ IsActivated, IsBlocked, TelegramUserId, SubscriptionToken]
├─*───1─ AppRole (ровно одна роль; роль несёт квоту MaxConfigs)
├─1───*─ VpnConfig
*─┐
├─1─ Inbound ─*─1─ Node
│ │ └─*───*─ AppRole (какие роли могут создавать конфиги в инбаунде)
│ └─*─ TrafficSample
└─1─ Inbound ─*─1─ Node
└─*───*─ AppRole (какие роли могут создавать конфиги в инбаунде)
├─0..1─* ActivationRequest (запрос активации у админа, с комментарием)
├─1───*─ TelegramLinkToken (короткоживущие токены привязки)
└─0..1─* TelegramLoginRequest (passwordless-вход)
Plan ─1───*─ VpnConfig (опционально; квота по числу конфигов — на роли, не на Plan)
VpnConfig ─*─ TrafficSample (история трафика; пишется TrafficSyncService)
AuditLog (append-only журнал действий; ссылается на ActorId/TargetId)
ClientApp (каталог приложений-клиентов; группируется по OperatingSystem)
```
@@ -79,41 +82,48 @@ ClientApp (каталог приложений-клиен
| `ClientExternalId` | `string` | Идентификатор клиента, который вернула панель (UUID для VLESS/VMess, пароль для Trojan/Shadowsocks — ThreeXui.Net отдаёт его как string) |
| `Protocol` | `VpnProtocol` | Денормализовано с inbound |
| `DeviceLimit` | `int` | Лимит одновременных устройств/IP (0 = без лимита); задаёт юзер → `limitIp` в 3x-ui |
| `TrafficLimit` | `TrafficLimit` (VO) | Лимит в байтах (0 = безлимит) |
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui |
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui (только для отображения — лимит трафика не применяется) |
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
| `ExpiresAt` | `DateTimeOffset?`| null = бессрочно |
| `ExpiresAt` | `DateTimeOffset?`| Зарезервировано на будущее — в MVP ничего его не выставляет, конфиг живёт бессрочно |
| `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`|
| `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` |
| `LastSyncAt` | `DateTimeOffset?`| |
| `CreatedAt` | `DateTimeOffset` | |
Инварианты и переходы:
- Создаётся в статусе `Active`; поля клиента в 3x-ui и запись в БД создаются атомарно (компенсация при сбое).
- **Проверка квоты выполняется в транзакции с блокировкой** (иначе два параллельных создания пробьют лимит).
- `Revoke()` → удаляет клиента в 3x-ui, статус `Revoked` (запись остаётся для истории/аудита).
- `Rotate()` → перевыпуск: удаляет старого клиента в 3x-ui и создаёт нового (новый UUID/ссылка);
квоту **не тратит**. Для случая утечки ссылки.
- `Disable()`/`Enable()` → отключение/включение клиента в 3x-ui без удаления (используется при блокировке юзера).
Инварианты и переходы (методы на `VpnConfig`, `backend/src/PnvPanel.Domain/Configs/VpnConfig.cs`):
- `Create(...)` статус `Active`, `ClientExternalId` пуст до ответа от 3x-ui; хендлер вызывает
`IXuiPanelGateway.AddClientAsync`, затем `AssignRemoteClient(id)` и сохраняет — при сбое БД после
успешного создания в панели хендлер удаляет клиента в 3x-ui (компенсация).
- **Проверка квоты выполняется под `pg_advisory_xact_lock(hashtext(userId))`** в транзакции создания
(`CreateVpnConfigCommandHandler`) — иначе два параллельных запроса могли бы пробить лимит роли.
- `Revoke()` → статус `Revoked` (запись остаётся для истории/аудита); хендлер отдельно удаляет клиента в 3x-ui.
- `Rotate(newClientEmail, newClientExternalId)` → перевыпуск: хендлер создаёт нового клиента в 3x-ui,
удаляет старого, генерирует новый `SubscriptionToken`; квоту **не тратит**. Для случая утечки ссылки.
- `Disable()`/`Enable()` → меняют только статус записи (`Active ↔ Disabled`); отключение/включение
самого клиента в 3x-ui делает хендлер отдельным вызовом гейтвея (используется при блокировке юзера).
- `Rename(label)` / `SetDeviceLimit(n)` → юзер меняет метку и лимит устройств (последнее синкается в `limitIp` 3x-ui).
- Синхронизация: если `Used ≥ TrafficLimit``LimitReached` (+ событие); если `now ≥ ExpiresAt``Expired`.
- `UpdateTraffic(up, down)` → пишет `TrafficSyncService` при периодической синхронизации, только для отображения.
- **Создание разрешено только активированному пользователю** (`AppUser.IsActivated == true`).
- Число активных конфигов пользователя не может превышать **квоту его роли** (`AppRole.MaxConfigs`;
роль `admin` — без лимита). У пользователя ровно одна роль. См. `AppRole` ниже.
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
### Plan — тариф (опционально, backlog)
Шаблон лимитов трафика/срока для конфига. **Квота на число конфигов — это `AppRole.MaxConfigs`,
а не Plan.** Plan остаётся опциональным механизмом для лимитов трафика/срока и в MVP не обязателен.
> **Не реализовано в MVP**: лимиты трафика и автоматическое истечение срока конфига. `ExpiresAt`
> никогда не выставляется, `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда
> не переводит конфиг — оставлено на будущее (см. `Plan` ниже и Backlog в [vision.md](vision.md)).
### Plan — тариф (backlog, не реализовано)
Планировался как шаблон лимитов трафика/срока для конфига — **квота на число конфигов уже
реализована через `AppRole.MaxConfigs`, это не Plan**. Сущности `Plan` в коде нет; таблица ниже —
эскиз на будущее, если/когда лимиты трафика/срока понадобятся.
| Поле | Тип | Заметки |
| ------------------ | ----------- | ------------------------------ |
| `Id` | `Guid` | PK |
| `Name` | `string` | |
| `TrafficLimit` | `TrafficLimit` (VO) | Байты |
| `TrafficLimitBytes`| `long` | 0 = безлимит |
| `DurationDays` | `int?` | Срок действия конфига |
| `MaxConfigs` | `int` | Сколько конфигов даёт тариф |
| `IsActive` | `bool` | |
### TrafficSample — история трафика (для графиков)
@@ -139,7 +149,7 @@ ClientApp (каталог приложений-клиен
| `Id` | `Guid` | PK |
| `Name` | `string` | Название, напр. «v2rayNG», «Hiddify», «NekoBox» |
| `DownloadUrl` | `Uri` | Ссылка на скачивание/стор |
| `OperatingSystem` | `OsPlatform` | `iOS` / `Android` / `Windows` / `MacOS` / `Linux` |
| `OperatingSystem` | `OsPlatform` | `IOS` / `Android` / `Windows` / `MacOS` / `Linux` |
| `Description` | `string?` | Короткая подсказка (опц.) |
| `IconUrl` | `string?` | Иконка (опц.) |
| `SortOrder` | `int` | Порядок внутри группы ОС |
@@ -253,9 +263,12 @@ UI **настойчиво напоминает** привязать его (ед
## Value Objects
- **NodeCredentials** — `Username` + `ProtectedPassword` (шифротекст); равенство по значению; пароль не сериализуется наружу.
- **TrafficLimit** — байты; помощники `IsUnlimited`, `IsExceededBy(used)`, форматирование в ГБ.
- **ConnectionLink** — построенная ThreeXui.Net строка подключения + производные (подписка, QR-payload).
- **NodeCredentials** (`Nodes/NodeCredentials.cs`)`Username` + `ProtectedPassword` (шифротекст,
`ISecretProtector`/ASP.NET Data Protection); пароль не сериализуется наружу.
Connection string для клиента строит `IXuiPanelGateway` (обёртка над `ThreeXui.Net`) на лету при
запросе `GET /api/configs/{id}/link` — отдельного value object под это не заводили. QR-код из
готовой строки генерируется **на фронте** (`qrcode.react`), сервер картинку не рендерит.
## Enums
@@ -266,22 +279,31 @@ enum ConfigStatus { Active, Disabled, Expired, LimitReached, Revoked }
enum TelegramLoginStatus { Pending, Approved, Rejected, Expired, Consumed }
enum ActivationStatus { Pending, Approved, Rejected }
enum AuditSource { Web, Telegram, System }
enum OsPlatform { iOS, Android, Windows, MacOS, Linux }
enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
```
## Доменные события
## Уведомления и аудит (без диспетчера доменных событий)
| Событие | Когда | Реакция |
| ----------------------- | --------------------------------------- | --------------------------------------------------- |
| `VpnConfigCreated` | Успешно создан конфиг | Realtime-пуш владельцу; аудит |
| `VpnConfigRevoked` | Конфиг отозван | Realtime-пуш; аудит |
| `TrafficLimitReached` | `Used ≥ Limit` при синхронизации | (опц.) отключить клиента в 3x-ui; пуш; статус |
| `NodeWentOffline` | Health-probe вернул недоступность | Пуш группе `admins`; пометка статуса |
| `ActivationRequested` | Пользователь запросил активацию | Пуш `admins` + уведомление админам в Telegram |
| `UserActivated` | Админ одобрил активацию | Пуш владельцу + Telegram-DM (если привязан); аудит |
| `UserBlocked` / `UserUnblocked` | Админ (раз)блокировал пользователя | Отключить/включить конфиги в 3x-ui; пуш + Telegram-DM; аудит |
| `VpnConfigRotated` | Пользователь перевыпустил конфиг | Новый линк владельцу; аудит |
В `Domain` нет маркера `IDomainEvent` и диспетчера событий — упрощение относительно исходного плана.
CQRS-хендлеры сами вызывают порты `IRealtimeNotifier` / `ITelegramNotifier` и пишут `AuditLog`
напрямую, после того как изменение состояния сохранено. Так проще проследить, что именно произойдёт
при вызове конкретной команды — не нужно искать обработчик события где-то ещё.
События публикуются из сущностей/хендлеров и обрабатываются `IDomainEventHandler<T>` в Application
(диспетчеризация — собственным диспетчером после `SaveChanges`); внешние эффекты (SignalR, 3x-ui) —
через порты, реализуемые в Infrastructure.
| Хендлер / фоновый сервис | Что происходит |
| ------------------------------------ | -------------------------------------------------------------------------- |
| `CreateVpnConfigCommandHandler` | Создаёт клиента в 3x-ui + `VpnConfig` |
| `RevokeVpnConfigCommandHandler` / `RotateVpnConfigCommandHandler` | Меняют клиента в 3x-ui и запись |
| `RequestActivationCommandHandler` | Realtime `activationRequested` группе `admins` + Telegram-уведомление админам (`AdminTelegramUserIds`) |
| `ApproveActivationCommandHandler` | `AuditLog` (`ActivationApproved`); realtime `userActivated` владельцу + Telegram-DM, если привязан |
| `RejectActivationCommandHandler` | `AuditLog` (`ActivationRejected`) |
| `BlockUserCommandHandler` / `UnblockUserCommandHandler` | Отключают/включают все активные конфиги в 3x-ui; `AuditLog`; Telegram-DM владельцу |
| `ChangeUserRoleCommandHandler` | `AuditLog` (`UserRoleChanged`) |
| `ForceRevokeConfigCommandHandler` | Отзывает конфиг в 3x-ui; `AuditLog` (`ConfigForceRevoked`); Telegram-DM владельцу |
| `ResetUserPasswordCommandHandler` | `AuditLog` (`UserPasswordReset`) |
| `RegisterNodeCommandHandler` / `UpdateNodeCommandHandler` / `DeleteNodeCommandHandler` | `AuditLog` (`NodeRegistered`/`NodeUpdated`/`NodeDeleted`) |
| `PublishInboundCommandHandler` | `AuditLog` (`InboundPublished`/`InboundUnpublished`) |
| `NodeHealthCheckService` (фон) | Обновляет `NodeStatus`; realtime `nodeStatusChanged` группе `admins` |
| `TrafficSyncService` (фон) | `UpdateTraffic(...)`; realtime `configTrafficUpdated` владельцу |
SignalR-события и группы — см. [architecture.md](architecture.md#realtime-signalr) и
[api-design.md](api-design.md#signalr--hub-hubspanel).