Refactor environment configuration and update documentation for MVP status
- 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:
+64
-42
@@ -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).
|
||||
|
||||
Reference in New Issue
Block a user