Update documentation and clarify MVP status
CI / Backend (build + test) (push) Successful in 1m33s
CI / Frontend (lint + typecheck + build) (push) Successful in 29s

- Revised the CLAUDE.md and README.md files to reflect the current MVP status, emphasizing completed features and intentionally omitted elements such as traffic limits and billing.
- Enhanced clarity in the documentation regarding the architecture, tech stack, and user roles.
- Removed the outdated roadmap section and streamlined references to tech stack decisions.
- Updated API design documentation to clarify the absence of versioning in the MVP and the handling of configuration details.
This commit is contained in:
Leonid Pershin
2026-07-02 21:11:59 +03:00
parent 012d08e737
commit ad94c6ef22
12 changed files with 144 additions and 426 deletions
+4 -22
View File
@@ -1,33 +1,15 @@
# PnvPanel — Документация
**MVP реализован** (backend M0M8 + полный frontend). Документация ниже описывает систему как она
реально построена, со сверенными по коду деталями — не первоначальный план. Расхождения с ранним
замыслом отмечены явно там, где это важно (например, домен изначально закладывал доменные события —
в реализации от них отказались в пользу прямых вызовов из CQRS-хендлеров, см. [architecture.md](architecture.md)).
Документация описывает систему как она построена: архитектура, домен, конвенции кода, как запускать
и как пользоваться.
Индекс. Читать в этом порядке для погружения:
1. **[Product Vision & Scope](vision.md)** — продукт, роли, пользовательские сценарии, границы MVP.
1. **[Product Vision & Scope](vision.md)** — продукт, роли, пользовательские сценарии, функциональность.
2. **[Architecture](architecture.md)** — Clean Architecture, слои, CQRS, интеграция с 3x-ui, realtime, безопасность, фоновые задачи.
3. **[Domain Model](domain-model.md)** — сущности, value objects, связи, инварианты, уведомления/аудит.
4. **[Tech Stack (ADR)](tech-stack.md)** — принятые решения по технологиям и их обоснование.
4. **[Tech Stack](tech-stack.md)** — используемые технологии и ключевые решения по домену.
5. **[Backend Conventions](backend-conventions.md)** — структура решения, паттерны, соглашения по коду.
6. **[Frontend](frontend.md)** — стек, структура, работа с API и realtime.
7. **[Telegram Bot](telegram-bot.md)** — бот: ссылка на сайт, просмотр конфигов, passwordless-вход через привязку Telegram.
8. **[API Design](api-design.md)** — контракты REST и SignalR.
9. **[Roadmap](roadmap.md)** — ретроспектива по этапам (milestones) + backlog.
## Принятые решения
Ключевые развилки закрыты (полная таблица — в [tech-stack.md](tech-stack.md#принятые-решения-по-открытым-вопросам)):
- **CQRS** — собственный тонкий диспетчер (не MediatR).
- **Роли** — ровно одна роль на пользователя; квота = `MaxConfigs` роли.
- **Секреты нод** — ASP.NET Core Data Protection (шифрование at-rest).
- **Тарифы `Plan`** — backlog (в MVP конфиги без лимитов трафика/срока).
- **i18n** — RU + EN с первого дня (react-i18next).
- **Telegram** — long polling; только привязка аккаунта (signup из бота — backlog).
- **История трафика** — простая таблица + TTL-чистка.
- **Логирование** — Serilog.
Остаточные мелочи (не блокируют старт): значение TTL истории трафика, прод-синки Serilog, TTL токенов Telegram.
+2 -2
View File
@@ -5,7 +5,7 @@ REST поверх HTTP/JSON, авторизация — `Authorization: Bearer <
(`items`, `total`, `page`, `pageSize`) — используется не везде, см. таблицы ниже. Все даты — ISO-8601 UTC.
Тела запросов/ответов — camelCase JSON; енумы сериализуются строками (`"Active"`, не `0`).
Базовый префикс: `/api` (**без версионирования в MVP**). Схема генерируется нативным
Базовый префикс: `/api` (без версионирования). Схема генерируется нативным
`Microsoft.AspNetCore.OpenApi` (`/openapi/v1.json`) и Scalar UI (`/scalar`) — каждый эндпоинт
аннотирован `.Produces<T>()`, так что схема полностью описывает и тела запросов, и тела ответов.
Ниже — полный контракт, сверенный построчно с кодом (`backend/src/PnvPanel.Api/Endpoints/*.cs`).
@@ -74,7 +74,7 @@ rate-limit'ом (`RateLimiting:AuthPermitLimit`, по умолчанию 20 за
**Нет отдельного `GET /api/configs/{id}`** — детали конфига берутся из списка `GET /api/configs`.
`VpnConfigDto`: `{ id, label, protocol, location, deviceLimit, usedUpBytes, usedDownBytes, expiresAt,
status, createdAt }`. `expiresAt` в MVP всегда `null` (лимиты по сроку не реализованы — см.
status, createdAt }`. `expiresAt` всегда `null` (лимиты по сроку не реализованы — см.
[domain-model.md](domain-model.md)). Ссылка подключения **не приходит вместе с созданием** — фронт
запрашивает `GET .../link` отдельно, по кнопке на карточке конфига; QR строится на фронте из
`connectionString`.
+11 -13
View File
@@ -44,14 +44,13 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
`Application`, реализуемые в `Infrastructure`.
### 1. `PnvPanel.Domain`
Ядро без внешних зависимостей. Никакого диспетчера доменных событий нет — это сознательное упрощение
относительно исходного плана, см. ниже.
Ядро без внешних зависимостей. Диспетчера доменных событий нет — уведомления и аудит вызываются
напрямую из CQRS-хендлеров (см. [domain-model.md](domain-model.md#уведомления-и-аудит-без-диспетчера-доменных-событий)).
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `ActivationRequest`, `ClientApp`, `AuditLog`,
`TelegramLinkToken`, `TelegramLoginRequest`, `TrafficSample` (см. [domain-model.md](domain-model.md)).
- **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды). Это единственный VO
`TrafficLimit`/`ConnectionLink` из раннего плана не понадобились (лимиты трафика — backlog,
connection string строит `IXuiPanelGateway` на лету).
- **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды) единственный VO;
connection string строит `IXuiPanelGateway` на лету, лимиты трафика не реализованы.
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`, `ActivationStatus`, `AuditSource`,
`TelegramLoginStatus`, `OsPlatform`.
- **Domain Exceptions**: `DomainException` — брошенный при нарушении инварианта в самой сущности
@@ -73,8 +72,7 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
- **Validators**: FluentValidation на команды, где есть что проверять помимо типов (не на все — см.
[backend-conventions.md](backend-conventions.md)).
- **DTO**: плоские `record`, конвертация из сущностей — статический метод `FromDomain(...)` на самом
DTO. Mapster из исходного плана не пригодился — при таком числе полей ручной маппинг читается
не хуже конфига маппера и не создаёт лишней зависимости.
DTO, без маппера (Mapster/AutoMapper).
- **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция +
`SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` нет — авторизация (роль,
активация) — это либо `RequireAuthorization()`/`RequireRole(...)` на эндпоинте, либо явная проверка
@@ -162,12 +160,12 @@ POST /api/configs
к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`.
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
- **Дрейф с 3x-ui в MVP не реконсилируется активно**: `TrafficSyncService` при недоступной ноде или
- **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
в 3x-ui в обход панели, локальная запись `VpnConfig` продолжит существовать до следующего
явного действия пользователя/админа (`Revoke`/`Rotate`), которое обнаружит несоответствие по ответу
гейтвея. Активная сверка/алертинг по дрейфу — задел на будущее, не реализовано.
гейтвея. Активной сверки/алертинга по дрейфу нет.
## Telegram-бот (presentation-адаптер)
@@ -204,8 +202,8 @@ POST /api/configs
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
шлёт `nodeStatusChanged` группе `admins`.
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера — для
нагрузки MVP этого достаточно (см. [tech-stack.md](tech-stack.md)).
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера
(см. [tech-stack.md](tech-stack.md)).
## Сидирование и старт
@@ -302,8 +300,8 @@ PostgreSQL:
вне нашего compose; `app` внутри отдаёт HTTP. Приложение доверяет `X-Forwarded-Proto/For` через
`ForwardedHeaders`-middleware — иначе Secure-cookie и определение схемы за прокси работать не будут.
Отдельный nginx/Caddy в compose **не** вводим.
- **Миграции**: применяются **автоматически на старте** приложения (в MVP; при масштабировании на
несколько инстансов — вынести в отдельный шаг/джобу).
- **Миграции**: применяются **автоматически на старте** приложения. При масштабировании на несколько
инстансов миграции стоит вынести в отдельный шаг/джобу.
- Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения,
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`).
+2 -2
View File
@@ -107,8 +107,8 @@ backend/
- **Result-модель**: команды/запросы возвращают `Result`/`Result<T>`; `ResultExtensions.ToHttpResult()`
мапит `Error.Type` в HTTP-статус на границе Api.
- **Транзакция на команду**: `UnitOfWorkBehavior` вызывает `SaveChangesAsync` после хендлера команды
(не запросов) — отдельной BEGIN/COMMIT-транзакции вокруг этого в MVP нет, полагаемся на то, что
один `SaveChanges` уже атомарен для одной единицы работы.
(не запросов) — отдельной BEGIN/COMMIT-транзакции вокруг этого нет, полагаемся на то, что один
`SaveChanges` уже атомарен для одной единицы работы.
- **Компенсация при частичном сбое**: если клиент успешно создан в 3x-ui, а `SaveChanges` в БД упал —
хендлер вызывает `RemoveClientAsync`, чтобы не оставить сироту в панели.
- **Защита от гонок на квоте — `pg_advisory_xact_lock`**, не оптимистичная блокировка: перед проверкой
+8 -23
View File
@@ -4,8 +4,7 @@
`AppUser`/`AppRole` — часть Identity (живут в `Infrastructure`, т.к. расширяют `IdentityUser<Guid>`/
`IdentityRole<Guid>`); чистый `PnvPanel.Domain` ссылается на пользователя/роль только по `Guid`.
Ниже — то, что реально реализовано и работает. Тарифы `Plan` и лимиты трафика на конфиг
(`TrafficLimit`) были в первоначальном плане, но остались в backlog — квота в MVP только одна:
Тарифы `Plan` и лимиты трафика на конфиг (`TrafficLimit`) не реализованы — единственная квота:
число активных конфигов на роль (`AppRole.MaxConfigs`).
## Диаграмма связей
@@ -84,7 +83,7 @@ ClientApp (каталог приложений-клиен
| `DeviceLimit` | `int` | Лимит одновременных устройств/IP (0 = без лимита); задаёт юзер → `limitIp` в 3x-ui |
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui (только для отображения — лимит трафика не применяется) |
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
| `ExpiresAt` | `DateTimeOffset?`| Зарезервировано на будущее — в MVP ничего его не выставляет, конфиг живёт бессрочно |
| `ExpiresAt` | `DateTimeOffset?`| Зарезервировано, сейчас ничего его не выставляет конфиг живёт бессрочно |
| `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`|
| `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` |
| `LastSyncAt` | `DateTimeOffset?`| |
@@ -109,22 +108,9 @@ ClientApp (каталог приложений-клиен
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
> **Не реализовано в MVP**: лимиты трафика и автоматическое истечение срока конфига. `ExpiresAt`
> никогда не выставляется, `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда
> не переводит конфиг — оставлено на будущее (см. `Plan` ниже и Backlog в [vision.md](vision.md)).
### Plan — тариф (backlog, не реализовано)
Планировался как шаблон лимитов трафика/срока для конфига — **квота на число конфигов уже
реализована через `AppRole.MaxConfigs`, это не Plan**. Сущности `Plan` в коде нет; таблица ниже —
эскиз на будущее, если/когда лимиты трафика/срока понадобятся.
| Поле | Тип | Заметки |
| ------------------ | ----------- | ------------------------------ |
| `Id` | `Guid` | PK |
| `Name` | `string` | |
| `TrafficLimitBytes`| `long` | 0 = безлимит |
| `DurationDays` | `int?` | Срок действия конфига |
| `IsActive` | `bool` | |
> Лимиты трафика и автоматическое истечение срока конфига не реализованы. `ExpiresAt` никогда не
> выставляется; `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда не переводит
> конфиг. Квота на число конфигов реализована через `AppRole.MaxConfigs` (см. [tech-stack.md](tech-stack.md)).
### TrafficSample — история трафика (для графиков)
Точки потребления во времени; пишутся синхронизацией.
@@ -137,8 +123,7 @@ ClientApp (каталог приложений-клиен
| `UpBytes` | `long` | Накопительно или дельта |
| `DownBytes` | `long` | |
> **Решение**: обычная таблица PostgreSQL + **TTL** — фоновая чистка записей старше N дней
> (`TrafficRetentionService`). TimescaleDB/агрегация — вне MVP.
> Обычная таблица PostgreSQL + **TTL** — фоновая чистка записей старше N дней (`TrafficRetentionService`).
### ClientApp — каталог приложений для подключения
Приложения-клиенты, которые админ рекомендует пользователям. На странице инструкций отображаются
@@ -284,8 +269,8 @@ enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
## Уведомления и аудит (без диспетчера доменных событий)
В `Domain` нет маркера `IDomainEvent` и диспетчера событий — упрощение относительно исходного плана.
CQRS-хендлеры сами вызывают порты `IRealtimeNotifier` / `ITelegramNotifier` и пишут `AuditLog`
В `Domain` нет маркера `IDomainEvent` и диспетчера событий. CQRS-хендлеры сами вызывают порты
`IRealtimeNotifier` / `ITelegramNotifier` и пишут `AuditLog`
напрямую, после того как изменение состояния сохранено. Так проще проследить, что именно произойдёт
при вызове конкретной команды — не нужно искать обработчик события где-то ещё.
+2 -3
View File
@@ -28,9 +28,8 @@ SPA на **React 19 + Vite + TypeScript**. Общается с бэком по R
| Линт | oxlint (не ESLint) |
| Пакетный менеджер | pnpm |
Установлены, но **не используются в MVP**: `recharts` (админская статистика — карточки с цифрами,
без графиков), `@tanstack/react-table` (админские таблицы написаны руками, без TanStack Table).
Оставлены как задел, если/когда понадобятся графики трафика или сложные таблицы с сортировкой.
Установлены, но не используются: `recharts` (админская статистика — карточки с цифрами, без
графиков), `@tanstack/react-table` (админские таблицы написаны руками, без TanStack Table).
## Структура
-136
View File
@@ -1,136 +0,0 @@
# Roadmap
**MVP полностью реализован** — все этапы M0–M8 закрыты. Ниже — ретроспектива по этапам (как было
задумано → что реально сделано, с честными пометками о расхождениях) и раздел [Backlog](#backlog-после-mvp)
с тем, что осталось за рамками MVP осознанно.
## M0 — Каркас и инфраструктура ✅
- Solution (`PnvPanel.slnx`) + 4 проекта (Domain/Application/Infrastructure/Api), ссылки по Clean Architecture.
- `Directory.Build.props`, `.editorconfig`, nullable включены. `dotnet format` — локальная команда,
в CI **не** запускается (CI гоняет только build/test).
- EF Core + Npgsql, миграции.
- Scaffolding фронта: Vite + React + TS + Tailwind + shadcn-стиль поверх Radix + TanStack Query/Router;
**тема light/dark/system** (провайдер + переключатель); i18n (RU/EN); dev-прокси `/api`,`/hubs` на бэк.
- **Единый контейнер**: multi-stage Dockerfile (node → dotnet publish → aspnet), Api раздаёт SPA из
`wwwroot` (fallback на `index.html`); docker-compose `app` + `db` (PostgreSQL); `ForwardedHeaders`
(TLS — внешним прокси); авто-применение миграций на старте.
- Health-check `/health`, Serilog, нативный OpenAPI (`Microsoft.AspNetCore.OpenApi`) + Scalar (без Swashbuckle).
- **CI (GitHub Actions)**: `dotnet build/test` + `pnpm build/lint/typecheck` (без деплоя).
- **Готово, когда**: единый образ поднимается в docker-compose рядом с postgres, отдаёт SPA и `/health`,
есть миграции, CI зелёный. ✅ Достигнуто — включая полную проверку `docker compose up` end-to-end.
## M1 — Аутентификация и сидинг ✅
- ASP.NET Core Identity (`AppUser`/`AppRole` c `MaxConfigs`); `DbInitializer`: системные роли
`admin`/`user` и учётка админа из env ([`.env.example`](../.env.example)).
- **Вход по username** (email не используется); JWT access + refresh (httpOnly cookie, ротация,
`Secure` по факту HTTPS-запроса, хранение хэшей), Identity lockout, rate-limit на `/auth/*`;
смена пароля. Явного анти-CSRF токена нет — обоснование в [architecture.md](architecture.md#безопасность).
- Регистрация: новый пользователь → роль `user`, `IsActivated = false`.
- Фронт: страницы login/register (username), стор авторизации, refresh-flow, guard-маршруты.
- **Готово, когда**: регистрация/вход/refresh/logout по username работают, админ засидан, новый юзер неактивен. ✅ Достигнуто.
## M2 — Роли и активация ✅
- Домен: динамические роли (CRUD, квота `MaxConfigs`), `ActivationRequest`.
- Команды/запросы: CreateRole/UpdateRole/DeleteRole, ChangeUserRole (одна роль), RequestActivation
(с комментарием), ApproveActivation/RejectActivation.
- Эндпоинты активации (user + admin) и ролей; проверка активации/роли — inline в хендлерах
и `RequireAuthorization(...)` на эндпоинте, без отдельных именованных policy.
- Фронт: экран «запросить активацию» (с комментарием), админ-очередь запросов, управление ролями/назначением.
- **Готово, когда**: юзер запрашивает активацию с комментарием, админ на сайте активирует; роли с квотами работают. ✅ Достигнуто.
## M3 — Ноды и публикация inbounds (по ролям) ✅
- Домен `Node`/`Inbound` (+ `AllowedRoles`, `DisplayName`); порт `IXuiPanelGateway` + единственная
реализация `XuiPanelGateway` (кэш клиента per-node внутри неё, `ThreeXui.Net`); шифрование секретов
нод (`ISecretProtector`/ASP.NET Data Protection).
- Команды/запросы: RegisterNode, UpdateNode, DeleteNode, SyncNode, ProbeNode, ListNodes, ListInbounds,
PublishInbound (с выбором ролей).
- Админка нод/инбаундов на фронте (публикация с `displayName` и `allowedRoleIds`).
- **Готово, когда**: админ подключает реальную 3x-ui и публикует inbound для выбранных ролей. ✅ Достигнуто
(удаление ноды с активными конфигами пока не блокируется — известный пробел, см. [tech-stack.md](tech-stack.md)).
## M4 — Конфиги пользователя (ядро продукта) ✅
- Домен `VpnConfig` (создание, отзыв, ротация; инварианты: активирован + квота роли (грандфазеринг)
+ доступ роли к инбаунду; квота — под `pg_advisory_xact_lock`; схема `ClientEmail`).
- CreateVpnConfig (с `label`/`deviceLimit``limitIp`), EditVpnConfig, RotateVpnConfig, RevokeVpnConfig,
GetMyConfigs, GetConfigLink, ListAvailableInbounds; connection string по запросу (не сразу при
создании), QR строится на фронте.
- Подписка: один эндпоинт `/sub/{token}` — токен либо агрегированный (`AppUser.SubscriptionToken`,
все конфиги), либо по одному конфигу (`VpnConfig.SubscriptionToken`); заголовки
`Subscription-Userinfo` / `Profile-Update-Interval`.
- Самоудаление аккаунта (`DELETE /api/auth/me`): отзыв всех активных конфигов + удаление `AppUser`.
- Каталог приложений `ClientApp` (домен + `GET /api/apps` по ОС; сид из `seed/client-apps.json`) + **страница инструкций** на фронте.
- Фронт: дашборд (метки, лимит устройств), создание/редактирование, страница инструкций, ссылка/QR
по кнопке, отзыв, перевыпуск, настройки аккаунта.
- **Готово, когда**: активированный юзер создаёт рабочий конфиг в доступном инбаунде в пределах квоты;
работает подписка. ✅ Достигнуто. Лимиты трафика/срока конфига — не реализованы, backlog
(см. [domain-model.md](domain-model.md)).
## M5 — Синхронизация трафика и realtime ✅
- `TrafficSyncService` (обход включённых нод, обновление трафика, `TrafficSample`) — только для
отображения, без активной реконсиляции дрейфа (недоступная нода/незнакомый клиент — тихо пропускаются).
- `NodeHealthCheckService`; `TrafficRetentionService` (TTL-чистка истории).
- SignalR `PanelHub` + `IRealtimeNotifier` (реализован в `Api/Hubs/`, не в Infrastructure); события
`configTrafficUpdated`/`configStatusChanged`/`nodeStatusChanged`/`activationRequested`/`userActivated`.
- Фронт: живые обновления трафика/статусов без перезагрузки. Реакции на превышение лимита/срока нет —
таких лимитов не существует (см. M4).
- **Готово, когда**: трафик и статусы обновляются в UI без перезагрузки. ✅ Достигнуто.
## M6 — Админ-статистика, управление пользователями, аудит ✅
- ListUsers, BlockUser (→ отключение конфигов в 3x-ui) / UnblockUser (два отдельных эндпоинта),
ChangeUserRole, ResetUserPassword, GetUserConfigs, ForceRevokeConfig, GetStats.
- `AuditLog`: запись значимых действий (активация, блокировка, смена роли, ноды/инбаунды — источник
всегда `Web`, т.к. пишется из тех же хендлеров, что вызывает и бот) + эндпоинт `/api/admin/audit`.
- Каталог приложений: админ-CRUD `ClientApp` (`/api/admin/apps`) — название, ссылка, ОС, порядок, вкл/выкл.
- Фронт: таблицы пользователей/ролей/нод/приложений (обычные `<table>`, без TanStack Table), журнал
аудита, статистика карточками (без графиков — `recharts` установлен, но не подключён).
- **Готово, когда**: админ видит статистику и журнал, управляет пользователями/ролями/конфигами;
блокировка гасит VPN. ✅ Достигнуто.
## M7 — Telegram-бот ✅
- Библиотека Telegram.Bot, `TelegramBotHostedService` (long polling) в процессе Api, `IOptions<TelegramOptions>`.
- Домен: поля Telegram у `AppUser`, `TelegramLinkToken`, `TelegramLoginRequest`.
- Флоу привязки (`LinkTelegramCommand`) + эндпоинт `link-token`/`unlink`.
- Регистрация прямо из бота (`RegisterViaTelegramCommand`, кнопка «📝 Зарегистрироваться» при `/start`
и в местах, где боту нужен привязанный аккаунт): логин — `@username` из Telegram, при отсутствии
или занятости — Telegram id; пароль генерируется (`RandomNumberGenerator`, гарантированы заглавная
буква/строчная/цифра под текущую политику пароля) и присылается в чат один раз. Новый аккаунт —
роль `user`, `IsActivated = false`, активация как у обычной регистрации. Логин можно сменить в
Настройках (`ChangeUserNameCommand`, `POST /api/auth/change-username`) — актуально, если логин
получился числовым (Telegram id).
- Passwordless-вход: `login-request` + подтверждение в боте (`ApproveTelegramLoginCommand`) → выпуск JWT; поллинг завершения на фронте (`GET /api/auth/telegram/login-request/{id}`).
- Команды бота: `/start` (+ `link_<token>`/`login_<requestId>` deep-link payload), «Мои конфиги»
(`/configs` — по сообщению на конфиг, с inline-кнопкой «🔗 Показать ссылку», раскрывающей connection
string по запросу через тот же `GetConfigLinkQuery`, что и веб; ссылка не выводится сразу в списке,
чтобы не светиться в истории чата без явного действия юзера), `/unlink`, `/requests`, `/help`.
- **Админ в боте**: уведомления о запросах активации + inline «Активировать/Отклонить», `/requests` (по Telegram id из env).
- **DM-уведомления юзеру**: активация (`ApproveActivationCommandHandler`), блокировка (`BlockUserCommandHandler`), принудительный отзыв конфига админом (`ForceRevokeConfigCommandHandler`) — если Telegram привязан. Бот — read-only по конфигам (только просмотр/показ ссылки, без создания/ротации/отзыва).
- **Готово, когда**: юзер привязывает Telegram, входит без пароля, видит конфиги; админ активирует запросы прямо в боте. ✅ Достигнуто.
- **Перенесено в backlog** (не реализовано в MVP): восстановление пароля через бота (`/resetpassword` с одноразовой ссылкой) — сейчас сброс пароля только через админа (`ResetUserPasswordCommand`); QR-картинкой в сообщениях бота (пока только текстовая ссылка).
Фронтовые кнопки «Войти через Telegram»/«Привязать Telegram» реализованы в отдельной итерации (см. M0 фронт).
## M8 — Закалка (hardening) ✅
- Тесты: `PnvPanel.Domain.Tests` (54, чистые unit-тесты инвариантов сущностей), `PnvPanel.Application.Tests`
(71, CQRS-хендлеры на EF Core InMemory + NSubstitute-моки портов), `PnvPanel.IntegrationTests`
(Testcontainers.PostgreSql + `WebApplicationFactory<Program>` — реальный HTTP-контракт, включая
проверку `pg_advisory_xact_lock` под параллельной нагрузкой на квоту конфигов).
- Rate-limiting, аудит-лог, единообразные `ProblemDetails`, ретеншн `TrafficSample` — сделаны в M5/M6.
- CI (`.github/workflows/ci.yml`): `dotnet build/test` (backend, включая интеграционные — на
`ubuntu-latest` Docker доступен) + `pnpm lint/typecheck/build` (frontend), без деплоя.
- Прод-`docker-compose.yml`: `env_file: .env` прокидывает все секреты в контейнер `app`, том
`dp_keys` для key-ring Data Protection (переживает пересоздание контейнера), healthcheck `app`
через `GET /health` (curl добавлен в runtime-образ). TLS — внешним прокси (без изменений).
- **Готово, когда**: зелёный CI, покрытие ключевых сценариев, готовность к деплою. ✅ Достигнуто
(интеграционные тесты прогнаны локально через Testcontainers после появления Docker на машине
разработки — 134/134 зелёных; там же впервые собран и проверен единый Docker-образ и
docker-compose стек end-to-end).
## Backlog (после MVP)
- Полное самообслуживание в боте (создание/ротация/отзыв конфигов) — в MVP бот read-only.
- Telegram Login Widget как альтернатива кнопке-боту (сама регистрация/вход через бота уже реализованы —
см. M7 и [telegram-bot.md](telegram-bot.md)).
- Тарифы/биллинг/платежи, автопродление, промокоды.
- Реферальная программа; расширенные уведомления (через Telegram/веб — email в проекте не используется).
- Балансировка/выбор оптимальной ноды, автоскейл.
- OpenTelemetry-трейсинг, метрики, дашборды.
- Вынос фоновых задач в Hangfire/Quartz; TimescaleDB для истории трафика.
- Мультиязычность (RU/EN и далее).
+93 -192
View File
@@ -1,210 +1,111 @@
# Tech Stack — решения и обоснование (ADR-lite)
Формат: **Решение** → короткое обоснование → альтернативы. Отклонения фиксировать здесь же.
# Tech Stack
## Backend
### Платформа: .NET 10 + ASP.NET Core Web API
Долгосрочная (LTS-класса) современная платформа, нативная поддержка Minimal API, rate limiting,
health checks, DI. `ThreeXui.Net` таргетит `net10.0` — совпадение целевого фреймворка.
### Архитектура: Clean Architecture (4 проекта)
`Domain / Application / Infrastructure / Api`. Тестируемость, изоляция домена, заменяемость инфраструктуры.
Альтернативы: Vertical Slice (проще для мелких API, но хуже изолирует домен для растущего продукта) —
можно комбинировать: слои + организация Application «по фичам».
### CQRS: собственный тонкий диспетчер ✅ (зафиксировано и реализовано)
**Решение принято**: свой `ISender` вместо MediatR (тот с v12 стал платным). `ISender.Send()`
резолвит `ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через `IPipelineBehavior<,>`.
Реализованы три поведения: `ValidationBehavior` (FluentValidation), `LoggingBehavior`,
`UnitOfWorkBehavior` (транзакция + `SaveChangesAsync` на команду). Плюсы: нет лицензий и внешних
зависимостей, полный контроль. Отклонены: MediatR (лицензия), FastEndpoints/Wolverine (лишняя связанность/переписывание модели).
**Отличие от исходного плана**: отдельного диспетчера доменных событий (`IDomainEventHandler<T>`) в
итоге не заводили — оказалось, что для текущего размера проекта прямые вызовы `IRealtimeNotifier`/
`ITelegramNotifier` и запись `AuditLog` прямо в хендлере команды читаются проще, чем публикация
события и поиск обработчика где-то ещё (см. [domain-model.md](domain-model.md#уведомления-и-аудит-без-диспетчера-доменных-событий)).
Также нет отдельного `AuthorizationBehavior` — роль проверяется на уровне эндпоинта
(`RequireAuthorization(...)`), а более тонкие проверки (владение, активация) — в самом хендлере.
### Валидация: FluentValidation
Декларативные валидаторы на команды/запросы, подключаются через `ValidationBehavior`. Заводится не
для каждой команды — только там, где есть что проверить помимo типов (например, у команд без
пользовательского ввода валидатора нет).
### Маппинг: вручную, без Mapster
В исходном плане был Mapster — на практике для такого числа полей ручной статический метод
`XxxDto.FromDomain(entity)` на самом DTO читается не хуже конфига маппера и не добавляет
зависимость. `Mapster` в проект так и не попал.
### ORM: EF Core 10 + Npgsql
Миграции, LINQ, `IEntityTypeConfiguration`. Провайдер PostgreSQL — Npgsql.
Запросы-чтения — проекции в DTO (`AsNoTracking` + `Select`).
### БД: PostgreSQL
Надёжная, богатая по типам (jsonb, массивы), бесплатная. Для истории трафика в будущем —
TimescaleDB-расширение.
### Auth: ASP.NET Core Identity + JWT
Identity для пользователей/ролей/хэширования; JWT access (короткий TTL) + refresh (httpOnly cookie, ротация).
Альтернатива — внешний OIDC (Keycloak/Auth0); отклонено на этом этапе в пользу полного контроля.
### RBAC: динамические роли с квотой (`AppRole.MaxConfigs`)
Роли — стандартный Identity, но `AppRole` расширен `MaxConfigs`. Админ создаёт/назначает роли;
доступ к инбаундам — по ролям (`Inbound.AllowedRoles`). Квота на число конфигов — на роли, а не на `Plan`.
### Активация пользователей
`AppUser.IsActivated` + `ActivationRequest` (с комментарием). Неактивированный не создаёт конфиги.
Решение принимает админ на сайте или в Telegram — одними и теми же CQRS-командами.
### Сидинг из env
Идемпотентный `DbInitializer` на старте: системные роли (`admin`/`user`), учётка админа и Telegram id
админов — из переменных окружения. Пример — [`.env.example`](../.env.example). Строго типизированные
`IOptions<T>` с валидацией на старте.
### Realtime: SignalR
Нативно для ASP.NET Core, авто-транспорт (WebSocket→SSE→long-poll), группы/пользователи, JWT-авторизация хабов.
### Telegram-бот: Telegram.Bot (in-process hosted service)
Де-факто стандартная C#-библиотека. Бот хостится в процессе Api как `BackgroundService` (условие
единого контейнера) и вызывает те же CQRS-хендлеры, что и REST. Транспорт — **long polling** для
MVP (не нужен публичный webhook, проще в одиночном контейнере); webhook — опция для прод (с секретным
заголовком). Passwordless-вход выпускает те же JWT/refresh, что и веб. Детали — [telegram-bot.md](telegram-bot.md).
### Фоновые задачи: BackgroundService + PeriodicTimer (MVP)
Без внешних зависимостей для MVP. При росте (ретраи, расписания, дашборд) — **Hangfire** или **Quartz.NET**.
### Result-модель: собственный `Result<T>` (или ErrorOr)
Явные ошибки вместо исключений для управляемых сценариев; исключения — только для действительно исключительного.
### Логирование: Serilog ✅ (зафиксировано)
**Решение принято**: структурное логирование — **Serilog** (`Serilog.AspNetCore`), настройка через
`appsettings`/env, `UseSerilogRequestLogging()` + `Enrich.FromLogContext()`. Синк MVP — Console.
Секреты (пароли, JWT, `BotToken`) в логи не попадают. **Не реализовано**: явное обогащение контекста
полями `UserId`/`NodeId`/`ConfigId`, сквозной `CorrelationId`, rolling file/Seq/OTel-экспорт — было в
исходном плане, осталось в backlog. Сегодня для расследования инцидента доступны только то, что даёт
`Enrich.FromLogContext()` + запрос/ответ из request-логирования.
### API-документация: нативный OpenAPI (`Microsoft.AspNetCore.OpenApi`) + Scalar UI
`AddOpenApi()`/`MapOpenApi()` — встроенная в ASP.NET Core (.NET 9+) генерация схемы, без Swashbuckle.
`/openapi/v1.json` используется фронтом для `pnpm gen:api` (openapi-typescript). `/scalar` — Scalar UI
вместо Swagger UI. Каждый эндпоинт аннотирован `.Produces<T>()`, чтобы схема полностью описывала
и тела запросов, и тела ответов.
### Тесты: xUnit + NSubstitute + Testcontainers
Юнит-тесты домена/хендлеров (без FluentAssertions — обычные `Assert.*` из xUnit хватает для
используемых проверок), интеграционные — с реальным PostgreSQL в Testcontainers
(`Testcontainers.PostgreSql` + `WebApplicationFactory<Program>`).
- **Платформа**: .NET 10, ASP.NET Core Web API (Minimal API).
- **Архитектура**: Clean Architecture, 4 проекта — `Domain / Application / Infrastructure / Api`.
- **CQRS**: собственный тонкий диспетчер (`ISender`), без MediatR. `ISender.Send()` резолвит
`ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через `IPipelineBehavior<,>`:
`ValidationBehavior` (FluentValidation), `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция +
`SaveChangesAsync` на команду). Авторизация проверяется на уровне эндпоинта
(`RequireAuthorization(...)`), более тонкие проверки (владение, активация) — в хендлере.
- **Валидация**: FluentValidation, подключается через `ValidationBehavior` (не для каждой команды —
только там, где есть что проверить помимо типов).
- **Маппинг**: вручную, статический метод `XxxDto.FromDomain(entity)` на самом DTO.
- **ORM**: EF Core 10 + Npgsql. Миграции, `IEntityTypeConfiguration`. Запросы-чтения — проекции в DTO
(`AsNoTracking` + `Select`).
- **БД**: PostgreSQL.
- **Auth**: ASP.NET Core Identity + JWT (access, короткий TTL) + refresh (httpOnly cookie, ротация).
- **RBAC**: динамические роли с квотой (`AppRole.MaxConfigs`). Доступ к инбаундам — по ролям
(`Inbound.AllowedRoles`). Квота на число конфигов — на роли, а не на тариф.
- **Активация пользователей**: `AppUser.IsActivated` + `ActivationRequest` (с комментарием).
Неактивированный не создаёт конфиги; решение принимает админ на сайте или в Telegram — одними и
теми же CQRS-командами.
- **Сидинг из env**: идемпотентный `DbInitializer` на старте — системные роли (`admin`/`user`),
учётка админа и Telegram id админов. Пример — [`.env.example`](../.env.example).
- **Realtime**: SignalR — авто-транспорт (WebSocket→SSE→long-poll), группы/пользователи,
JWT-авторизация хабов.
- **Telegram-бот**: Telegram.Bot, хостится в процессе Api как `BackgroundService` (long polling) и
вызывает те же CQRS-хендлеры, что и REST. Passwordless-вход выпускает те же JWT/refresh, что и веб.
Детали — [telegram-bot.md](telegram-bot.md).
- **Фоновые задачи**: `BackgroundService` + `PeriodicTimer`, без внешних зависимостей.
- **Ошибки**: собственный `Result<T>` вместо исключений для управляемых сценариев; исключения — только
для действительно исключительного.
- **Логирование**: Serilog (`Serilog.AspNetCore`), `UseSerilogRequestLogging()` +
`Enrich.FromLogContext()`. Секреты (пароли, JWT, `BotToken`) в логи не попадают.
- **API-документация**: нативный OpenAPI (`Microsoft.AspNetCore.OpenApi`) + Scalar UI, без Swashbuckle.
`/openapi/v1.json` используется фронтом для `pnpm gen:api` (openapi-typescript). `/scalar` — UI.
- **Тесты**: xUnit + NSubstitute + Testcontainers (юнит-тесты домена/хендлеров, интеграционные — с
реальным PostgreSQL через `Testcontainers.PostgreSql` + `WebApplicationFactory<Program>`).
## Frontend
### React 19 + Vite + TypeScript
Максимальная экосистема, быстрый dev-сервер и сборка Vite, строгая типизация. SPA (не SSR) —
для внутренней панели SSR избыточен и усложняет деплой рядом с C# API.
### Данные с сервера: TanStack Query
Кэш, инвалидация, фоновые рефетчи, статусы загрузки/ошибок. Идеально для CRUD-панели.
### Роутинг: TanStack Router
Типобезопасный роутинг, интеграция с TanStack Query. Альтернатива — React Router 7.
### UI: shadcn/ui + Tailwind CSS v4
Копируемые в проект, полностью кастомизируемые компоненты (Radix под капотом), современный вид,
тёмная тема из коробки. Иконки — `lucide-react`.
### Клиентский стейт: Zustand
Лёгкий стор для глобального (авторизация, тема). Серверный стейт — только в TanStack Query.
### Формы: react-hook-form + zod
Производительные формы + схемная валидация; те же zod-схемы для типобезопасности API-ответов.
### Realtime: @microsoft/signalr
Официальный клиент SignalR; подписки на события хаба обновляют кэш TanStack Query.
### Типы API: openapi-typescript ✅ (зафиксировано)
`pnpm gen:api` гоняет `openapi-typescript` по `/openapi/v1.json` живого бэкенда →
`shared/api/schema.gen.ts`. На практике фичи импортируют типы из руками написанного
`shared/api/types.ts` (см. [frontend.md](frontend.md)) — он логически совпадает со сгенерированной
схемой (сверено), но даёт нормальные generic (`PagedList<T>`) и понятные имена, которых нет в JSON
Schema. `orval` рассматривался как альтернатива (codegen хуков), не использовался.
### Графики: Recharts (установлен, графики не построены)
Библиотека в зависимостях фронта на будущее — в MVP админская статистика показана карточками с
цифрами, без графиков. QR-коды конфигов — `qrcode.react` (реально используется).
### i18n: react-i18next, RU + EN ✅ (зафиксировано)
**Решение принято**: локализация с первого дня, языки **RU + EN** (RU по умолчанию). Тексты — через
ключи (`react-i18next`), не хардкод строк в компонентах.
- **React 19 + Vite + TypeScript** — SPA, без SSR.
- **Данные с сервера**: TanStack Query — кэш, инвалидация, фоновые рефетчи, статусы загрузки/ошибок.
- **Роутинг**: TanStack Router — типобезопасный, интеграция с TanStack Query.
- **UI**: shadcn/ui + Tailwind CSS v4 (компоненты на Radix), тёмная/светлая тема. Иконки —
`lucide-react`.
- **Клиентский стейт**: Zustand — только для авторизации и темы; серверный стейт — в TanStack Query.
- **Формы**: react-hook-form + zod.
- **Realtime**: `@microsoft/signalr` — подписки на события хаба обновляют кэш TanStack Query.
- **Типы API**: `pnpm gen:api` гоняет `openapi-typescript` по `/openapi/v1.json` живого бэкенда →
`shared/api/schema.gen.ts`. Фичи импортируют типы из руками написанного `shared/api/types.ts`
(см. [frontend.md](frontend.md)) — даёт нормальные generic (`PagedList<T>`) и понятные имена.
- **QR-коды**: `qrcode.react`.
- **i18n**: react-i18next, языки RU + EN (RU по умолчанию). Тексты — через ключи, не хардкод строк.
## Инфраструктура
### Упаковка: единый образ приложения + PostgreSQL
По требованию — **один контейнер на всё приложение** (REST + SignalR + Telegram-бот + статика SPA)
и отдельный контейнер БД.
Единый образ приложения (REST + SignalR + Telegram-бот + статика SPA) + отдельный контейнер PostgreSQL.
- **Multi-stage Dockerfile**: (1) `node` собирает фронт → `dist/`; (2) `dotnet sdk` публикует Api и
копирует статику в `wwwroot`; (3) `aspnet` runtime запускает Api. Api раздаёт SPA (`UseStaticFiles`
+ fallback на `index.html`), фронт и бек — один origin.
- **docker-compose**: `app` (единый образ) + `db` (PostgreSQL) с томом.
- Почему не отдельный nginx: единый origin упрощает CORS/куки/деплой и укладывается в требование
«фронт+бек в одном контейнере».
- **TLS — внешний** (решение): HTTPS терминирует внешний прокси/шлюз (nginx/Traefik/cloud LB) вне
compose; `app` отдаёт HTTP и доверяет `X-Forwarded-*` через `ForwardedHeaders`. Свой nginx/Caddy не вводим.
- **Миграции** — авто на старте приложения (MVP).
- **CI** — GitHub Actions, **только сборка/тесты**: `dotnet build`/`test`, `pnpm build`/`lint`/`typecheck`.
Публикация образа и деплой — вручную/позже (в MVP не автоматизируем).
- **Пакетный менеджер фронта**: pnpm (быстрый, экономный по диску).
- Отдельного nginx для статики нет — единый origin упрощает CORS/куки/деплой.
- **TLS — внешний**: HTTPS терминирует внешний прокси/шлюз (nginx/Traefik/cloud LB) вне compose;
`app` отдаёт HTTP и доверяет `X-Forwarded-*` через `ForwardedHeaders`.
- **Миграции** — применяются автоматически на старте приложения.
- **CI** — GitHub Actions: `dotnet build`/`test`, `pnpm build`/`lint`/`typecheck`. Без деплоя.
- **Пакетный менеджер фронта**: pnpm.
## Принятые решения (по открытым вопросам)
## Ключевые решения по домену и поведению
Все ключевые развилки закрыты:
| Тема | Как сделано |
| -------------------------- | --------------------------------------------------------------------------------- |
| Ролей у пользователя | Ровно одна роль (квота = `MaxConfigs` роли) |
| Секреты нод | ASP.NET Core Data Protection (шифрование at-rest, key-ring на томе) |
| Тарифы/лимиты трафика | Не реализованы — конфиги без лимитов трафика/срока |
| i18n | RU + EN (react-i18next) |
| Telegram-транспорт | Long polling |
| Регистрация через Telegram | Поддержана (логин — Telegram `@username`/id, пароль генерируется и присылается в чат) |
| История трафика | Простая таблица PostgreSQL (`TrafficSample`) + TTL-чистка (`TrafficRetentionService`) |
| Логирование | Serilog (Console) |
| Вход | По username (email не используется; SMTP не нужен) |
| Восстановление пароля | Через привязанный Telegram (self-service); без привязки — сброс админом |
| Регистрация | Открытая + гейт активации админом |
| Конфиги в одном инбаунде | Разрешено несколько (ограничение — только общая квота роли) |
| Данные ноды пользователю | Показываем только `DisplayName` + протокол; адрес/хост/порт скрыты |
| Блокировка пользователя | Отключает все его конфиги в 3x-ui (`Disabled`); разблокировка — включает обратно |
| Понижение роли | Грандфазеринг: существующие конфиги живут, новые нельзя до входа в квоту |
| Скоуп Telegram-бота | Read-only по конфигам (создание/отзыв — на сайте); DM-уведомления юзеру |
| Подписка | Агрегированная на юзера (`AppUser.SubscriptionToken`) + по конфигу |
| Аудит | `AuditLog` (append-only): активация, блокировка, смена роли, отзыв, ноды/инбаунды |
| Ротация конфига | `Rotate()` — перевыпуск UUID/ссылки, квоту не тратит (на случай утечки) |
| Лимит устройств | Per-config, задаёт юзер (`DeviceLimit``limitIp` в 3x-ui; 0 = без лимита) |
| Метка конфига | `Label` — пользователь именует конфиг («Мой телефон») |
| Самоудаление аккаунта | Отзыв всех активных конфигов в 3x-ui + удаление `AppUser` |
| Версионирование API | Без версий (`/api` без `v1`) |
| Подписка (заголовки) | `Subscription-Userinfo` (used/total/expire) + `profile-update-interval` |
| Тема сайта | Светлая + тёмная (+ системная); выбор в localStorage |
| Инструкции/приложения | Отдельная страница инструкций + каталог `ClientApp` (админ CRUD, юзер — по ОС) |
| Реконсиляция с 3x-ui | `TrafficSyncService` молча пропускает ноду/клиента при недоступности или несовпадении, без активной реконсиляции (см. [architecture.md](architecture.md)) |
| # | Вопрос | Решение |
| - | ------------------------------ | ------------------------------------------------------------------- |
| 1 | CQRS-медиатор | **Собственный тонкий диспетчер** (не MediatR) |
| 2 | Ролей у пользователя | **Ровно одна роль** (квота = `MaxConfigs` роли) |
| 3 | Секреты нод | **ASP.NET Core Data Protection** (шифрование at-rest, key-ring на томе) |
| 4 | Тарифы `Plan` в MVP | **Backlog** — в MVP конфиги без лимитов трафика/срока |
| 5 | i18n | **RU + EN** с первого дня (react-i18next) |
| 6 | Telegram-транспорт | **Long polling** |
| 7 | Регистрация через Telegram | **Только привязка** существующего аккаунта (signup из бота — backlog) |
| 8 | История трафика `TrafficSample`| **Простая таблица PostgreSQL + TTL** (фоновая чистка старше N дней) |
| 9 | Логирование | **Serilog** (Console + rolling file) |
Также реализовано: Identity lockout по неудачным входам; проверка квоты конфигов под
`pg_advisory_xact_lock`; схема `ClientEmail = pnv_{userIdShort}_{rand}`. Явного анти-CSRF токена на
refresh-cookie нет — обоснование в [architecture.md](architecture.md#безопасность) (`SameSite=Strict`
+ `HttpOnly` достаточно при мутациях только по Bearer-токену). Удаление ноды с активными конфигами
**не блокируется** — известный пробел: `DeleteNodeCommandHandler` каскадно удаляет инбаунды ноды без
проверки существующих `VpnConfig`.
### Продуктовые решения (поведение)
| Тема | Решение |
| ------------------------ | ------------------------------------------------------------------------------- |
| Вход | **По username** (email в системе не используется; SMTP не нужен) |
| Восстановление пароля | Через привязанный Telegram (self-service); без привязки — сброс админом |
| Побуждение привязать TG | Настойчивый баннер/уведомления в UI, пока Telegram не привязан |
| Регистрация | Открытая + гейт активации админом |
| Конфиги в одном инбаунде | Разрешено несколько (ограничение — только общая квота роли) |
| Данные ноды пользователю | Показываем только `DisplayName` + протокол; адрес/хост/порт скрыты |
| Блокировка пользователя | Отключать все его конфиги в 3x-ui (`Disabled`); разблокировка — включить обратно |
| Понижение роли | **Грандфазеринг**: существующие конфиги живут, новые нельзя до входа в квоту |
| Скоуп Telegram-бота (MVP)| **Read-only** по конфигам (создание/отзыв — на сайте); DM-уведомления юзеру |
| Подписка | Агрегированная на юзера (`AppUser.SubscriptionToken`) + по конфигу |
| Аудит | `AuditLog` (append-only): активация, блокировка, смена роли, отзыв, ноды/инбаунды |
| Ротация конфига | `Rotate()` — перевыпуск UUID/ссылки, квоту не тратит (на случай утечки) |
| Лимит устройств | Per-config, задаёт юзер (`DeviceLimit``limitIp` в 3x-ui; 0 = без лимита) |
| Метка конфига | `Label` — пользователь именует конфиг («Мой телефон») |
| Самоудаление аккаунта | Разрешено: отзыв всех активных конфигов в 3x-ui + удаление `AppUser`. `AuditLog` уже хранит только `Guid` без PII — отдельной анонимизации задним числом нет, сам факт удаления в аудит тоже не пишется |
| Версионирование API | Без версий в MVP (`/api` без `v1`) |
| Подписка (заголовки) | `Subscription-Userinfo` (used/total/expire) + `profile-update-interval` |
| Тема сайта | Светлая + тёмная (+ системная); Tailwind `dark`, выбор в localStorage |
| Инструкции/приложения | Отдельная страница инструкций + каталог `ClientApp` (админ CRUD, юзер — по ОС); стартовый сид из `seed/client-apps.json` |
| Реконсиляция с 3x-ui | Не реализована активно — `TrafficSyncService` молча пропускает ноду/клиента при недоступности или несовпадении, без пометки дрейфа (см. [architecture.md](architecture.md)) |
Также реализовано: Identity lockout по неудачным входам; проверка квоты под `pg_advisory_xact_lock`;
схема `ClientEmail = pnv_{userIdShort}_{rand}`. Явного анти-CSRF токена на refresh-cookie нет (см.
[architecture.md](architecture.md#безопасность) — обоснование, почему `SameSite=Strict` + `HttpOnly`
достаточно при мутациях только по Bearer-токену). Удаление ноды с активными конфигами **не
блокируется** — это известный пробел, не защита: `DeleteNodeCommandHandler` каскадно удаляет
инбаунды ноды без проверки существующих `VpnConfig`.
Не реализовано (осталось на будущее, не блокирует текущую работу): TTL для истории трафика (сейчас
`TrafficRetentionService` работает, но точный порог не вынесен в решение — см. код); прод-синки
Serilog (файл/Seq/OTel) и структурное обогащение логов (`UserId`/`CorrelationId`); точные TTL
токенов Telegram. Email/SMTP в проекте **не используются** (вход по username, восстановление — через
Telegram/админа).
Email/SMTP в проекте не используются (вход по username, восстановление — через Telegram/админа).
+3 -3
View File
@@ -34,7 +34,7 @@ Telegram-бот — **второй канал доставки** (presentation-
(пусто — кнопки нет). Слэш-команды `/configs`/`/unlink` продолжают работать как раньше — кнопки лишь
вызывают те же обработчики через callback (`menu:configs`/`menu:unlink`/`menu:back`).
**Не реализовано / backlog:**
**Не реализовано:**
- QR-картинкой и агрегированная подписка в самом боте (только текстовая ссылка на конфиг по кнопке).
- Отдельная команда `/resetpassword` с одноразовой ссылкой — восстановление пароля сейчас идёт
только через обычный passwordless-вход (`/start login_<n>`) + смену пароля в настройках на сайте.
@@ -224,5 +224,5 @@ Username бота для deepLink (кнопка «Привязать Telegram»/
сидируется в БД и не связан с учёткой сид-админа (`AdminSeed:*`), это независимый список.
Сообщения бота **не локализованы** по языку пользователя — все тексты на русском независимо от языка
интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом
не реализована, backlog.
интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом
не реализована.
+5 -5
View File
@@ -75,8 +75,8 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
### U2. Пользователь следит за трафиком
- Фоновая синхронизация тянет трафик из 3x-ui; изменения приходят в UI через SignalR (без перезагрузки).
- **В MVP это только отображение**: лимиты по трафику/сроку конфига не реализованы — единственная
квота — число активных конфигов на роль. Конфиг живёт, пока его явно не отзовут.
- Это только отображение: лимиты по трафику/сроку конфига не реализованы — единственная квота —
число активных конфигов на роль. Конфиг живёт, пока его явно не отзовут.
### A1. Админ подключает ноду и публикует инбаунды
1. Вводит адрес панели 3x-ui, логин/пароль (шифруются при хранении).
@@ -97,9 +97,9 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
3. В следующий раз на сайте выбирает «Войти через Telegram» → подтверждает вход в боте → входит без пароля.
4. В боте может смотреть свои конфиги и открывать сайт.
## Границы MVP
## Функциональность
**В MVP входит:**
**Реализовано:**
- Регистрация/вход (JWT + Identity); сид админа из env.
- Динамические роли с квотой конфигов (сид `admin`/`user`); создание ролей и назначение админом.
- Активация пользователей по запросу с комментарием (одобрение на сайте и в Telegram).
@@ -112,7 +112,7 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
- Страница инструкций по подключению + каталог приложений по ОС (админ ведёт, юзер видит сгруппировано).
- Единый Docker-образ (фронт+бек) + PostgreSQL в docker-compose.
**За рамками MVP (backlog):**
**Не реализовано:**
- Тарифы/биллинг/платежи.
- Многоуровневые квоты, автопродление, промокоды.
- Балансировка нагрузки между нодами, автоскейл.