Update .gitignore to include local environment files and expand README with project details, tech stack, documentation links, and project status.
This commit is contained in:
@@ -0,0 +1,285 @@
|
||||
# Domain Model
|
||||
|
||||
Домен — «rich model»: инварианты и переходы состояний живут в сущностях, а не в хендлерах.
|
||||
`AppUser` — часть Identity (в `Infrastructure`); домен ссылается на пользователя по `UserId : Guid`.
|
||||
|
||||
## Диаграмма связей
|
||||
|
||||
```
|
||||
AppUser (Identity) [+ IsActivated, TelegramUserId]
|
||||
├─*───1─ AppRole (ровно одна роль; роль несёт квоту MaxConfigs)
|
||||
├─1───*─ VpnConfig
|
||||
│ *─┐
|
||||
│ ├─1─ Inbound ─*─1─ Node
|
||||
│ │ └─*───*─ AppRole (какие роли могут создавать конфиги в инбаунде)
|
||||
│ └─*─ TrafficSample
|
||||
├─0..1─* ActivationRequest (запрос активации у админа, с комментарием)
|
||||
├─1───*─ TelegramLinkToken (короткоживущие токены привязки)
|
||||
└─0..1─* TelegramLoginRequest (passwordless-вход)
|
||||
Plan ─1───*─ VpnConfig (опционально; квота по числу конфигов — на роли, не на Plan)
|
||||
AuditLog (append-only журнал действий; ссылается на ActorId/TargetId)
|
||||
ClientApp (каталог приложений-клиентов; группируется по OperatingSystem)
|
||||
```
|
||||
|
||||
## Сущности
|
||||
|
||||
### Node — VPN-сервер (панель 3x-ui)
|
||||
Подключённая администратором панель 3x-ui.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ---------------- | --------------- | ------------------------------------------------------------ |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `Name` | `string` | Отображаемое имя |
|
||||
| `BaseAddress` | `Uri` | Напр. `https://panel.example.com:2053/` |
|
||||
| `Credentials` | `NodeCredentials` (VO) | Логин + **зашифрованный** пароль (`ISecretProtector`) |
|
||||
| `Location` | `string?` | Страна/город/тег для выбора пользователем |
|
||||
| `Status` | `NodeStatus` | `Online` / `Offline` / `Unknown` |
|
||||
| `IsEnabled` | `bool` | Выключена админом → скрыта из самообслуживания |
|
||||
| `LastSyncAt` | `DateTimeOffset?` | Последняя успешная синхронизация |
|
||||
| `CreatedAt` | `DateTimeOffset`| |
|
||||
|
||||
Инварианты: `BaseAddress` абсолютный; при `IsEnabled == false` или `Status == Offline` **новые**
|
||||
конфиги на ноде запрещены, но **существующие не трогаем** (клиенты остаются в 3x-ui). Статус ноды
|
||||
показываем пользователю как индикатор «состояние сервера».
|
||||
|
||||
### Inbound — прокси-inbound на ноде
|
||||
Проекция inbound из 3x-ui; определяет протокол и параметры подключения.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ----------------- | ------------- | ---------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK (внутренний) |
|
||||
| `NodeId` | `Guid` | FK → Node |
|
||||
| `RemoteInboundId` | `int` | Id inbound в 3x-ui |
|
||||
| `Protocol` | `VpnProtocol` | `Vless` / `Vmess` / `Trojan` / `Shadowsocks` |
|
||||
| `Remark` | `string` | Метка из 3x-ui |
|
||||
| `Port` | `int` | |
|
||||
| `IsPublished` | `bool` | Доступен ли для самообслуживания пользователями |
|
||||
| `AllowedRoles` | `AppRole[]` (M:N) | Роли, которым разрешено создавать конфиги в этом инбаунде |
|
||||
| `DisplayName` | `string?` | Витринное имя для пользователя, напр. «Германия (Trojan)» |
|
||||
| `MaxClients` | `int?` | Лимит клиентов (null = без лимита) |
|
||||
| `LastSyncAt` | `DateTimeOffset?` | |
|
||||
|
||||
Инварианты: конфиг можно создать только если `IsPublished && Node.IsEnabled`, **роль пользователя
|
||||
входит в `AllowedRoles`**, и при заданном `MaxClients` он не достигнут. Публикация инбаунда админом
|
||||
включает выбор `AllowedRoles` (напр. «Германия (Trojan)» → роли `user`, `vip`).
|
||||
|
||||
> **Пользователю показываем только `DisplayName` + протокол.** Адрес/хост ноды, `RemoteInboundId`,
|
||||
> `Port` и прочие детали 3x-ui в пользовательские DTO не попадают (только в админские).
|
||||
|
||||
### VpnConfig — конфиг пользователя (клиент в 3x-ui)
|
||||
Центральная сущность. Одна запись = один клиент внутри inbound + его привязка к пользователю.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------ | ---------------- | -------------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (владелец) |
|
||||
| `InboundId` | `Guid` | FK → Inbound |
|
||||
| `Label` | `string?` | Пользовательская метка («Мой телефон»); редактируется юзером |
|
||||
| `ClientEmail` | `string` | Уникальный ключ клиента в 3x-ui; схема `pnv_{userIdShort}_{rand}` (уникален в рамках панели, виден владелец) |
|
||||
| `ClientUuid` | `Guid` | UUID клиента (VLESS/VMess) |
|
||||
| `Protocol` | `VpnProtocol` | Денормализовано с inbound |
|
||||
| `DeviceLimit` | `int` | Лимит одновременных устройств/IP (0 = без лимита); задаёт юзер → `limitIp` в 3x-ui |
|
||||
| `TrafficLimit` | `TrafficLimit` (VO) | Лимит в байтах (0 = безлимит) |
|
||||
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui |
|
||||
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
|
||||
| `ExpiresAt` | `DateTimeOffset?`| null = бессрочно |
|
||||
| `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 без удаления (используется при блокировке юзера).
|
||||
- `Rename(label)` / `SetDeviceLimit(n)` → юзер меняет метку и лимит устройств (последнее синкается в `limitIp` 3x-ui).
|
||||
- Синхронизация: если `Used ≥ TrafficLimit` → `LimitReached` (+ событие); если `now ≥ ExpiresAt` → `Expired`.
|
||||
- **Создание разрешено только активированному пользователю** (`AppUser.IsActivated == true`).
|
||||
- Число активных конфигов пользователя не может превышать **квоту его роли** (`AppRole.MaxConfigs`;
|
||||
роль `admin` — без лимита). У пользователя ровно одна роль. См. `AppRole` ниже.
|
||||
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
|
||||
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
|
||||
|
||||
### Plan — тариф (опционально, backlog)
|
||||
Шаблон лимитов трафика/срока для конфига. **Квота на число конфигов — это `AppRole.MaxConfigs`,
|
||||
а не Plan.** Plan остаётся опциональным механизмом для лимитов трафика/срока и в MVP не обязателен.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------ | ----------- | ------------------------------ |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `Name` | `string` | |
|
||||
| `TrafficLimit` | `TrafficLimit` (VO) | Байты |
|
||||
| `DurationDays` | `int?` | Срок действия конфига |
|
||||
| `MaxConfigs` | `int` | Сколько конфигов даёт тариф |
|
||||
| `IsActive` | `bool` | |
|
||||
|
||||
### TrafficSample — история трафика (для графиков)
|
||||
Точки потребления во времени; пишутся синхронизацией.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------ | ---------------- | -------------------------- |
|
||||
| `Id` | `long` | PK |
|
||||
| `ConfigId` | `Guid` | FK → VpnConfig |
|
||||
| `Timestamp` | `DateTimeOffset` | |
|
||||
| `UpBytes` | `long` | Накопительно или дельта |
|
||||
| `DownBytes` | `long` | |
|
||||
|
||||
> **Решение**: обычная таблица PostgreSQL + **TTL** — фоновая чистка записей старше N дней
|
||||
> (`TrafficRetentionService`). TimescaleDB/агрегация — вне MVP.
|
||||
|
||||
### ClientApp — каталог приложений для подключения
|
||||
Приложения-клиенты, которые админ рекомендует пользователям. На странице инструкций отображаются
|
||||
**сгруппированными по ОС**; клик открывает ссылку на скачивание.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ----------------- | ------------- | --------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `Name` | `string` | Название, напр. «v2rayNG», «Hiddify», «NekoBox» |
|
||||
| `DownloadUrl` | `Uri` | Ссылка на скачивание/стор |
|
||||
| `OperatingSystem` | `OsPlatform` | `iOS` / `Android` / `Windows` / `MacOS` / `Linux` |
|
||||
| `Description` | `string?` | Короткая подсказка (опц.) |
|
||||
| `IconUrl` | `string?` | Иконка (опц.) |
|
||||
| `SortOrder` | `int` | Порядок внутри группы ОС |
|
||||
| `IsEnabled` | `bool` | Показывать пользователям |
|
||||
|
||||
Управляется админом (CRUD). Пользователю отдаётся только `IsEnabled`, сгруппировано по `OperatingSystem`.
|
||||
|
||||
### AuditLog — журнал действий
|
||||
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------ | ---------------- | -------------------------------------------------------------- |
|
||||
| `Id` | `long` | PK |
|
||||
| `ActorId` | `Guid?` | Кто выполнил (null — система/фон) |
|
||||
| `Action` | `string` | Напр. `UserActivated`, `UserBlocked`, `RoleChanged`, `ConfigRevoked`, `NodeAdded`, `InboundPublished` |
|
||||
| `TargetType` | `string` | Сущность (`User`/`Config`/`Node`/`Inbound`/`Role`) |
|
||||
| `TargetId` | `string` | Идентификатор цели |
|
||||
| `Metadata` | `jsonb` | Доп. детали (старое/новое значение, комментарий) |
|
||||
| `Source` | `AuditSource` | `Web` / `Telegram` / `System` |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Пишется из хендлеров (или обработчиков доменных событий), append-only.
|
||||
|
||||
### AppUser — расширения (Identity)
|
||||
`AppUser` живёт в Identity (`Infrastructure`). **Логин — по `UserName`** (уникальный, обязательный).
|
||||
**Email в системе не используется** — поле не заполняем/не требуем (стандартная колонка Identity
|
||||
остаётся пустой). Помимо стандартных полей Identity:
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------------- | ----------------- | ---------------------------------------------------- |
|
||||
| `IsActivated` | `bool` | По умолчанию `false` при регистрации; активирует админ |
|
||||
| `ActivatedAt` | `DateTimeOffset?` | Когда активирован |
|
||||
| `ActivatedBy` | `Guid?` | Какой админ активировал |
|
||||
| `IsBlocked` | `bool` | Блокировка админом: вход запрещён + все конфиги отключены в 3x-ui |
|
||||
| `SubscriptionToken` | `string` | Секрет для **агрегированной** подписки `/sub/{token}` (все активные конфиги юзера) |
|
||||
| `TelegramUserId` | `long?` | Id пользователя Telegram; **уникальный**; null до привязки |
|
||||
| `TelegramUsername` | `string?` | @username на момент привязки (для отображения) |
|
||||
| `TelegramLinkedAt` | `DateTimeOffset?` | Когда привязан |
|
||||
|
||||
Инварианты: один `TelegramUserId` ↔ один аккаунт (повторная привязка требует `/unlink`);
|
||||
неактивированный пользователь не может создавать конфиги; при регистрации выдаётся роль `user`.
|
||||
**Блокировка** (`IsBlocked = true`) переводит все конфиги в `Disabled` (отключение клиентов в 3x-ui);
|
||||
разблокировка включает их обратно. У пользователя ровно одна роль.
|
||||
|
||||
**Восстановление пароля**: только через привязанный Telegram (passwordless-вход → смена пароля в
|
||||
настройках, либо reset-флоу в боте). Если Telegram не привязан — пароль сбрасывает **админ**
|
||||
(`ResetUserPasswordCommand`). Пока Telegram не привязан,
|
||||
UI **настойчиво напоминает** привязать его (единственный self-service способ восстановления).
|
||||
|
||||
### AppRole — роль с квотой (Identity, динамическая)
|
||||
Расширяет `IdentityRole<Guid>`. Роли **создаёт админ** и назначает пользователям; роль несёт квоту
|
||||
на число конфигов.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------ | -------- | --------------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `Name` | `string` | Напр. `admin`, `user`, `vip` |
|
||||
| `MaxConfigs` | `int` | Квота активных конфигов (для `admin` игнорируется — без лимита) |
|
||||
| `IsSystem` | `bool` | Системная (`admin`, `user`) — нельзя удалить/переименовать |
|
||||
|
||||
Сидируются: `admin` (без лимита) и `user` (`MaxConfigs` = `Roles__DefaultUserMaxConfigs`, по умолчанию 3).
|
||||
**У пользователя ровно одна роль**; его квота = `MaxConfigs` этой роли (`admin` → без лимита).
|
||||
|
||||
**Понижение роли (грандфазеринг)**: смену роли на роль с меньшей квотой разрешаем даже если текущих
|
||||
конфигов больше новой квоты — существующие конфиги сохраняются, но **создание новых блокируется**,
|
||||
пока число активных не станет меньше квоты. Форс-отзыв лишних не делаем.
|
||||
|
||||
### ActivationRequest — запрос активации
|
||||
Пользователь просит активацию у админа; админ одобряет/отклоняет на сайте или в Telegram.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------ | ----------------------- | ---------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `UserId` | `Guid` | FK → AppUser (заявитель) |
|
||||
| `Comment` | `string?` | Комментарий заявителя, напр. «я Никита» — чтобы админ понял, кто это |
|
||||
| `Status` | `ActivationStatus` | `Pending` / `Approved` / `Rejected` |
|
||||
| `DecidedBy` | `Guid?` | Админ, принявший решение |
|
||||
| `DecidedAt` | `DateTimeOffset?` | |
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
|
||||
Инварианты: одновременно не более одного `Pending`-запроса на пользователя; `Approved` →
|
||||
`AppUser.IsActivated = true`. Создание запроса и решение шлют realtime/Telegram-уведомления.
|
||||
|
||||
### TelegramLinkToken — токен привязки
|
||||
Короткоживущий одноразовый токен для флоу привязки Telegram.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------ | ----------------- | ---------------------------------------- |
|
||||
| `Id` | `Guid` | PK |
|
||||
| `Token` | `string` | Высокоэнтропийный секрет (в deep-link) |
|
||||
| `UserId` | `Guid` | FK → AppUser (кто привязывает) |
|
||||
| `ExpiresAt` | `DateTimeOffset` | ≈2–5 минут |
|
||||
| `ConsumedAt` | `DateTimeOffset?` | Одноразовый: гасится при использовании |
|
||||
|
||||
### TelegramLoginRequest — запрос passwordless-входа
|
||||
Запрос входа на сайт без пароля, подтверждаемый в боте.
|
||||
|
||||
| Поле | Тип | Заметки |
|
||||
| ------------ | ----------------------- | -------------------------------------------------------- |
|
||||
| `Id` | `Guid` | PK; `nonce` в deep-link |
|
||||
| `Status` | `TelegramLoginStatus` | `Pending` / `Approved` / `Rejected` / `Expired` / `Consumed` |
|
||||
| `UserId` | `Guid?` | Проставляется после подтверждения (по `TelegramUserId`) |
|
||||
| `Context` | `string?` | IP/устройство инициатора — показывается при подтверждении|
|
||||
| `CreatedAt` | `DateTimeOffset` | |
|
||||
| `ExpiresAt` | `DateTimeOffset` | ≈2–5 минут |
|
||||
|
||||
Переходы: `Pending → Approved/Rejected/Expired`; `Approved → Consumed` (после выпуска JWT сайту).
|
||||
После `Consumed`/`Expired` — не переиспользуется.
|
||||
|
||||
## Value Objects
|
||||
|
||||
- **NodeCredentials** — `Username` + `ProtectedPassword` (шифротекст); равенство по значению; пароль не сериализуется наружу.
|
||||
- **TrafficLimit** — байты; помощники `IsUnlimited`, `IsExceededBy(used)`, форматирование в ГБ.
|
||||
- **ConnectionLink** — построенная ThreeXui.Net строка подключения + производные (подписка, QR-payload).
|
||||
|
||||
## Enums
|
||||
|
||||
```csharp
|
||||
enum VpnProtocol { Vless, Vmess, Trojan, Shadowsocks }
|
||||
enum NodeStatus { Unknown, Online, Offline }
|
||||
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 }
|
||||
```
|
||||
|
||||
## Доменные события
|
||||
|
||||
| Событие | Когда | Реакция |
|
||||
| ----------------------- | --------------------------------------- | --------------------------------------------------- |
|
||||
| `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` | Пользователь перевыпустил конфиг | Новый линк владельцу; аудит |
|
||||
|
||||
События публикуются из сущностей/хендлеров и обрабатываются `IDomainEventHandler<T>` в Application
|
||||
(диспетчеризация — собственным диспетчером после `SaveChanges`); внешние эффекты (SignalR, 3x-ui) —
|
||||
через порты, реализуемые в Infrastructure.
|
||||
Reference in New Issue
Block a user