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
+13 -14
View File
@@ -11,18 +11,17 @@
хранит свою проекцию домена в PostgreSQL. Живые обновления — по SignalR. Приложение (фронт + бек + хранит свою проекцию домена в PostgreSQL. Живые обновления — по SignalR. Приложение (фронт + бек +
бот) поставляется **единым Docker-образом**; PostgreSQL — отдельным контейнером в compose. бот) поставляется **единым Docker-образом**; PostgreSQL — отдельным контейнером в compose.
> **Статус: MVP реализован и работает.** Бэкенд (M0–M8) и фронтенд полностью собраны, покрыты > Бэкенд и фронтенд полностью собраны и покрыты тестами (134 бэкенд-теста), единый Docker-образ и
> тестами (134 бэкенд-теста), единый Docker-образ и docker-compose стек проверены живьём. История > docker-compose стек проверены живьём. Осознанно не реализовано: тарифы, лимиты трафика/срока на
> этапов — [`docs/roadmap.md`](docs/roadmap.md); там же — раздел Backlog с тем, что осознанно > конфиг, полное самообслуживание в боте — см. [tech-stack.md](docs/tech-stack.md).
> оставлено за рамками MVP (тарифы, лимиты трафика/срока на конфиг, полное самообслуживание в боте и т.д.).
## Документация (single source of truth) ## Документация (single source of truth)
Прежде чем менять архитектуру или добавлять фичу — свериться с [`docs/`](docs/README.md): Прежде чем менять архитектуру или добавлять фичу — свериться с [`docs/`](docs/README.md):
- [Vision](docs/vision.md) · [Architecture](docs/architecture.md) · [Domain Model](docs/domain-model.md) - [Vision](docs/vision.md) · [Architecture](docs/architecture.md) · [Domain Model](docs/domain-model.md)
- [Tech Stack (ADR)](docs/tech-stack.md) · [Backend Conventions](docs/backend-conventions.md) - [Tech Stack](docs/tech-stack.md) · [Backend Conventions](docs/backend-conventions.md)
- [Frontend](docs/frontend.md) · [Telegram Bot](docs/telegram-bot.md) · [API Design](docs/api-design.md) · [Roadmap](docs/roadmap.md) - [Frontend](docs/frontend.md) · [Telegram Bot](docs/telegram-bot.md) · [API Design](docs/api-design.md)
**Держи доки в синхроне с кодом.** Меняешь контракт/архитектуру — обнови соответствующий док в том же изменении. **Держи доки в синхроне с кодом.** Меняешь контракт/архитектуру — обнови соответствующий док в том же изменении.
@@ -35,7 +34,7 @@
- **Frontend**: React 19 + Vite + TypeScript, TanStack Query/Router, shadcn-стиль поверх Radix + - **Frontend**: React 19 + Vite + TypeScript, TanStack Query/Router, shadcn-стиль поверх Radix +
Tailwind CSS v4, Zustand (только auth-стор), react-hook-form + zod, @microsoft/signalr. Пакетный Tailwind CSS v4, Zustand (только auth-стор), react-hook-form + zod, @microsoft/signalr. Пакетный
менеджер — pnpm, линтер — oxlint. `recharts`/`@tanstack/react-table` установлены, но не менеджер — pnpm, линтер — oxlint. `recharts`/`@tanstack/react-table` установлены, но не
используются в MVP (статистика — карточками, таблицы — руками). используются (статистика — карточками, таблицы — руками).
- **Telegram**: Telegram.Bot, бот как `BackgroundService` **в процессе Api** (long polling). - **Telegram**: Telegram.Bot, бот как `BackgroundService` **в процессе Api** (long polling).
- **Инфра**: единый Docker-образ (API + бот + статика SPA) + PostgreSQL в docker-compose. - **Инфра**: единый Docker-образ (API + бот + статика SPA) + PostgreSQL в docker-compose.
@@ -89,10 +88,10 @@
- **Аудит**: значимые действия (активация, блок, смена роли, отзыв, ноды/инбаунды) писать в `AuditLog` - **Аудит**: значимые действия (активация, блок, смена роли, отзыв, ноды/инбаунды) писать в `AuditLog`
(append-only, источник Web/Telegram/System). (append-only, источник Web/Telegram/System).
- **Подписка**: агрегированная на юзера (`AppUser.SubscriptionToken`, все активные конфиги) + по конфигу. - **Подписка**: агрегированная на юзера (`AppUser.SubscriptionToken`, все активные конфиги) + по конфигу.
- **Ротация конфига** (`Rotate()`): новый UUID/ссылка, квоту не тратит. **Бот в MVP — read-only** по конфигам. - **Ротация конфига** (`Rotate()`): новый UUID/ссылка, квоту не тратит. **Бот — read-only** по конфигам.
- **Конфиг**: пользователь задаёт метку (`Label`) и лимит устройств (`DeviceLimit``limitIp` в 3x-ui, 0=без лимита), может редактировать. - **Конфиг**: пользователь задаёт метку (`Label`) и лимит устройств (`DeviceLimit``limitIp` в 3x-ui, 0=без лимита), может редактировать.
- **Самоудаление аккаунта** (`DELETE /api/auth/me`): отзыв всех конфигов + удаление данных, аудит анонимизируется. - **Самоудаление аккаунта** (`DELETE /api/auth/me`): отзыв всех конфигов + удаление данных, аудит анонимизируется.
- **API без версионирования** в MVP (`/api` без `v1`). Подписка отдаёт `Subscription-Userinfo`. - **API без версионирования** (`/api` без `v1`). Подписка отдаёт `Subscription-Userinfo`.
- **Тема**: светлая/тёмная/системная (Tailwind `dark`, выбор в localStorage). - **Тема**: светлая/тёмная/системная (Tailwind `dark`, выбор в localStorage).
- **Инструкции + приложения**: отдельная страница инструкций; каталог `ClientApp` (админ CRUD: - **Инструкции + приложения**: отдельная страница инструкций; каталог `ClientApp` (админ CRUD:
название/ссылка/ОС/порядок/вкл), пользователю `GET /api/apps` отдаётся сгруппированным по ОС. название/ссылка/ОС/порядок/вкл), пользователю `GET /api/apps` отдаётся сгруппированным по ОС.
@@ -133,7 +132,7 @@
- Не вводи отдельный nginx-контейнер для статики без явной просьбы — это ломает требование единого контейнера. - Не вводи отдельный nginx-контейнер для статики без явной просьбы — это ломает требование единого контейнера.
- **TLS — внешний** (прокси/шлюз вне compose); `app` отдаёт HTTP + доверяет `X-Forwarded-*` через - **TLS — внешний** (прокси/шлюз вне compose); `app` отдаёт HTTP + доверяет `X-Forwarded-*` через
`ForwardedHeaders` (иначе Secure-cookie/схема за прокси сломаются). Свой nginx/Caddy не добавляй. `ForwardedHeaders` (иначе Secure-cookie/схема за прокси сломаются). Свой nginx/Caddy не добавляй.
- **Миграции** применяются авто на старте (MVP). **CI** (GitHub Actions) — только build/test, без деплоя. - **Миграции** применяются авто на старте. **CI** (GitHub Actions) — только build/test, без деплоя.
## Соглашения по коду ## Соглашения по коду
@@ -180,11 +179,11 @@ docker compose up -d # api + postgres (+ web)
> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент. > Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент.
> Пути — с учётом Windows. > Пути — с учётом Windows.
## Принятые решения (зафиксированы) ## Ключевые решения
Ключевые развилки закрыты — см. [tech-stack.md](docs/tech-stack.md#принятые-решения-по-открытым-вопросам): См. [tech-stack.md](docs/tech-stack.md#ключевые-решения-по-домену-и-поведению): CQRS —
CQRS — **собственный диспетчер** (не MediatR); **одна роль** на пользователя; секреты нод — **собственный диспетчер** (не MediatR); **одна роль** на пользователя; секреты нод —
**ASP.NET Data Protection**; тарифы `Plan` **backlog** (в MVP без лимитов трафика/срока); **ASP.NET Data Protection**; тарифы `Plan` не реализованы (нет лимитов трафика/срока на конфиг);
i18n — **RU+EN** (react-i18next); Telegram — **long polling**, только **привязка** (не signup); i18n — **RU+EN** (react-i18next); Telegram — **long polling**, только **привязка** (не signup);
история трафика — **простая таблица + TTL**; логирование — **Serilog**. история трафика — **простая таблица + TTL**; логирование — **Serilog**.
+1 -11
View File
@@ -50,25 +50,15 @@ pnpm dev # проксирует /api, /hubs на localhost:8080
- [Product Vision & Scope](docs/vision.md) — что мы строим и для кого - [Product Vision & Scope](docs/vision.md) — что мы строим и для кого
- [Architecture](docs/architecture.md) — Clean Architecture, CQRS, интеграция, realtime, безопасность - [Architecture](docs/architecture.md) — Clean Architecture, CQRS, интеграция, realtime, безопасность
- [Domain Model](docs/domain-model.md) — сущности, связи, инварианты - [Domain Model](docs/domain-model.md) — сущности, связи, инварианты
- [Tech Stack (ADR)](docs/tech-stack.md) — решения по стеку и их обоснование - [Tech Stack](docs/tech-stack.md) — используемые технологии
- [Backend Conventions](docs/backend-conventions.md) — структура проекта, паттерны, стиль кода - [Backend Conventions](docs/backend-conventions.md) — структура проекта, паттерны, стиль кода
- [Frontend](docs/frontend.md) — стек фронтенда и структура - [Frontend](docs/frontend.md) — стек фронтенда и структура
- [Telegram Bot](docs/telegram-bot.md) — бот, привязка Telegram и passwordless-вход - [Telegram Bot](docs/telegram-bot.md) — бот, привязка Telegram и passwordless-вход
- [API Design](docs/api-design.md) — REST-эндпоинты и SignalR-контракты - [API Design](docs/api-design.md) — REST-эндпоинты и SignalR-контракты
- [Roadmap](docs/roadmap.md) — этапы разработки (ретроспектива) + backlog
Пример переменных окружения (сид админа, БД, JWT, Telegram) — [`.env.example`](.env.example). Пример переменных окружения (сид админа, БД, JWT, Telegram) — [`.env.example`](.env.example).
Инструкции для AI-ассистента (Claude Code) — в [`CLAUDE.md`](CLAUDE.md). Инструкции для AI-ассистента (Claude Code) — в [`CLAUDE.md`](CLAUDE.md).
## Статус
**MVP реализован.** Backend (Clean Architecture, CQRS, 134 теста) и frontend (React SPA, полная
админка) готовы, покрывают весь [roadmap](docs/roadmap.md) M0–M8: регистрация/активация/роли,
управление нодами 3x-ui и inbound'ами, самообслуживание конфигами, realtime по SignalR, Telegram-бот
(привязка + passwordless-вход + уведомления), админ-статистика и аудит. Единый Docker-образ и
docker-compose стек проверены end-to-end. Что осталось за рамками MVP осознанно — раздел Backlog в
[roadmap.md](docs/roadmap.md#backlog-после-mvp).
## Лицензия ## Лицензия
[MIT](LICENSE) [MIT](LICENSE)
+4 -22
View File
@@ -1,33 +1,15 @@
# PnvPanel — Документация # 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, безопасность, фоновые задачи. 2. **[Architecture](architecture.md)** — Clean Architecture, слои, CQRS, интеграция с 3x-ui, realtime, безопасность, фоновые задачи.
3. **[Domain Model](domain-model.md)** — сущности, value objects, связи, инварианты, уведомления/аудит. 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)** — структура решения, паттерны, соглашения по коду. 5. **[Backend Conventions](backend-conventions.md)** — структура решения, паттерны, соглашения по коду.
6. **[Frontend](frontend.md)** — стек, структура, работа с API и realtime. 6. **[Frontend](frontend.md)** — стек, структура, работа с API и realtime.
7. **[Telegram Bot](telegram-bot.md)** — бот: ссылка на сайт, просмотр конфигов, passwordless-вход через привязку Telegram. 7. **[Telegram Bot](telegram-bot.md)** — бот: ссылка на сайт, просмотр конфигов, passwordless-вход через привязку Telegram.
8. **[API Design](api-design.md)** — контракты REST и SignalR. 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. (`items`, `total`, `page`, `pageSize`) — используется не везде, см. таблицы ниже. Все даты — ISO-8601 UTC.
Тела запросов/ответов — camelCase JSON; енумы сериализуются строками (`"Active"`, не `0`). Тела запросов/ответов — camelCase JSON; енумы сериализуются строками (`"Active"`, не `0`).
Базовый префикс: `/api` (**без версионирования в MVP**). Схема генерируется нативным Базовый префикс: `/api` (без версионирования). Схема генерируется нативным
`Microsoft.AspNetCore.OpenApi` (`/openapi/v1.json`) и Scalar UI (`/scalar`) — каждый эндпоинт `Microsoft.AspNetCore.OpenApi` (`/openapi/v1.json`) и Scalar UI (`/scalar`) — каждый эндпоинт
аннотирован `.Produces<T>()`, так что схема полностью описывает и тела запросов, и тела ответов. аннотирован `.Produces<T>()`, так что схема полностью описывает и тела запросов, и тела ответов.
Ниже — полный контракт, сверенный построчно с кодом (`backend/src/PnvPanel.Api/Endpoints/*.cs`). Ниже — полный контракт, сверенный построчно с кодом (`backend/src/PnvPanel.Api/Endpoints/*.cs`).
@@ -74,7 +74,7 @@ rate-limit'ом (`RateLimiting:AuthPermitLimit`, по умолчанию 20 за
**Нет отдельного `GET /api/configs/{id}`** — детали конфига берутся из списка `GET /api/configs`. **Нет отдельного `GET /api/configs/{id}`** — детали конфига берутся из списка `GET /api/configs`.
`VpnConfigDto`: `{ id, label, protocol, location, deviceLimit, usedUpBytes, usedDownBytes, expiresAt, `VpnConfigDto`: `{ id, label, protocol, location, deviceLimit, usedUpBytes, usedDownBytes, expiresAt,
status, createdAt }`. `expiresAt` в MVP всегда `null` (лимиты по сроку не реализованы — см. status, createdAt }`. `expiresAt` всегда `null` (лимиты по сроку не реализованы — см.
[domain-model.md](domain-model.md)). Ссылка подключения **не приходит вместе с созданием** — фронт [domain-model.md](domain-model.md)). Ссылка подключения **не приходит вместе с созданием** — фронт
запрашивает `GET .../link` отдельно, по кнопке на карточке конфига; QR строится на фронте из запрашивает `GET .../link` отдельно, по кнопке на карточке конфига; QR строится на фронте из
`connectionString`. `connectionString`.
+11 -13
View File
@@ -44,14 +44,13 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
`Application`, реализуемые в `Infrastructure`. `Application`, реализуемые в `Infrastructure`.
### 1. `PnvPanel.Domain` ### 1. `PnvPanel.Domain`
Ядро без внешних зависимостей. Никакого диспетчера доменных событий нет — это сознательное упрощение Ядро без внешних зависимостей. Диспетчера доменных событий нет — уведомления и аудит вызываются
относительно исходного плана, см. ниже. напрямую из CQRS-хендлеров (см. [domain-model.md](domain-model.md#уведомления-и-аудит-без-диспетчера-доменных-событий)).
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `ActivationRequest`, `ClientApp`, `AuditLog`, - **Entities**: `Node`, `Inbound`, `VpnConfig`, `ActivationRequest`, `ClientApp`, `AuditLog`,
`TelegramLinkToken`, `TelegramLoginRequest`, `TrafficSample` (см. [domain-model.md](domain-model.md)). `TelegramLinkToken`, `TelegramLoginRequest`, `TrafficSample` (см. [domain-model.md](domain-model.md)).
- **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды). Это единственный VO - **Value Objects**: `NodeCredentials` (логин + зашифрованный пароль ноды) единственный VO;
`TrafficLimit`/`ConnectionLink` из раннего плана не понадобились (лимиты трафика — backlog, connection string строит `IXuiPanelGateway` на лету, лимиты трафика не реализованы.
connection string строит `IXuiPanelGateway` на лету).
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`, `ActivationStatus`, `AuditSource`, - **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`, `ActivationStatus`, `AuditSource`,
`TelegramLoginStatus`, `OsPlatform`. `TelegramLoginStatus`, `OsPlatform`.
- **Domain Exceptions**: `DomainException` — брошенный при нарушении инварианта в самой сущности - **Domain Exceptions**: `DomainException` — брошенный при нарушении инварианта в самой сущности
@@ -73,8 +72,7 @@ PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **C
- **Validators**: FluentValidation на команды, где есть что проверять помимо типов (не на все — см. - **Validators**: FluentValidation на команды, где есть что проверять помимо типов (не на все — см.
[backend-conventions.md](backend-conventions.md)). [backend-conventions.md](backend-conventions.md)).
- **DTO**: плоские `record`, конвертация из сущностей — статический метод `FromDomain(...)` на самом - **DTO**: плоские `record`, конвертация из сущностей — статический метод `FromDomain(...)` на самом
DTO. Mapster из исходного плана не пригодился — при таком числе полей ручной маппинг читается DTO, без маппера (Mapster/AutoMapper).
не хуже конфига маппера и не создаёт лишней зависимости.
- **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция + - **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция +
`SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` нет — авторизация (роль, `SaveChangesAsync` на команду). Отдельного `AuthorizationBehavior` нет — авторизация (роль,
активация) — это либо `RequireAuthorization()`/`RequireRole(...)` на эндпоинте, либо явная проверка активация) — это либо `RequireAuthorization()`/`RequireRole(...)` на эндпоинте, либо явная проверка
@@ -162,12 +160,12 @@ POST /api/configs
к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`. к ноде. Cookie-session и авто-переавторизация на 401 обеспечиваются самой `ThreeXui.Net`.
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу. - Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды). - Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
- **Дрейф с 3x-ui в MVP не реконсилируется активно**: `TrafficSyncService` при недоступной ноде или - **Дрейф с 3x-ui активно не реконсилируется**: `TrafficSyncService` при недоступной ноде или
при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле при отсутствии клиента в ответе панели (`GetClientTrafficAsync`) просто пропускает его в этом цикле
синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо синхронизации — не помечает конфиг рассинхронизованным и не шлёт алерт. Если клиента удалили прямо
в 3x-ui в обход панели, локальная запись `VpnConfig` продолжит существовать до следующего в 3x-ui в обход панели, локальная запись `VpnConfig` продолжит существовать до следующего
явного действия пользователя/админа (`Revoke`/`Rotate`), которое обнаружит несоответствие по ответу явного действия пользователя/админа (`Revoke`/`Rotate`), которое обнаружит несоответствие по ответу
гейтвея. Активная сверка/алертинг по дрейфу — задел на будущее, не реализовано. гейтвея. Активной сверки/алертинга по дрейфу нет.
## Telegram-бот (presentation-адаптер) ## Telegram-бот (presentation-адаптер)
@@ -204,8 +202,8 @@ POST /api/configs
- **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`, - **NodeHealthCheckService** — health-probe нод (`IXuiPanelGateway.ProbeAsync`), обновляет `NodeStatus`,
шлёт `nodeStatusChanged` группе `admins`. шлёт `nodeStatusChanged` группе `admins`.
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика). - **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
- Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера — для - Реализованы как обычные `BackgroundService` + `PeriodicTimer`, без внешнего джоб-раннера
нагрузки MVP этого достаточно (см. [tech-stack.md](tech-stack.md)). (см. [tech-stack.md](tech-stack.md)).
## Сидирование и старт ## Сидирование и старт
@@ -302,8 +300,8 @@ PostgreSQL:
вне нашего compose; `app` внутри отдаёт HTTP. Приложение доверяет `X-Forwarded-Proto/For` через вне нашего compose; `app` внутри отдаёт HTTP. Приложение доверяет `X-Forwarded-Proto/For` через
`ForwardedHeaders`-middleware — иначе Secure-cookie и определение схемы за прокси работать не будут. `ForwardedHeaders`-middleware — иначе Secure-cookie и определение схемы за прокси работать не будут.
Отдельный nginx/Caddy в compose **не** вводим. Отдельный nginx/Caddy в compose **не** вводим.
- **Миграции**: применяются **автоматически на старте** приложения (в MVP; при масштабировании на - **Миграции**: применяются **автоматически на старте** приложения. При масштабировании на несколько
несколько инстансов — вынести в отдельный шаг/джобу). инстансов миграции стоит вынести в отдельный шаг/джобу.
- Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения, - Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения,
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`). JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`).
+2 -2
View File
@@ -107,8 +107,8 @@ backend/
- **Result-модель**: команды/запросы возвращают `Result`/`Result<T>`; `ResultExtensions.ToHttpResult()` - **Result-модель**: команды/запросы возвращают `Result`/`Result<T>`; `ResultExtensions.ToHttpResult()`
мапит `Error.Type` в HTTP-статус на границе Api. мапит `Error.Type` в HTTP-статус на границе Api.
- **Транзакция на команду**: `UnitOfWorkBehavior` вызывает `SaveChangesAsync` после хендлера команды - **Транзакция на команду**: `UnitOfWorkBehavior` вызывает `SaveChangesAsync` после хендлера команды
(не запросов) — отдельной BEGIN/COMMIT-транзакции вокруг этого в MVP нет, полагаемся на то, что (не запросов) — отдельной BEGIN/COMMIT-транзакции вокруг этого нет, полагаемся на то, что один
один `SaveChanges` уже атомарен для одной единицы работы. `SaveChanges` уже атомарен для одной единицы работы.
- **Компенсация при частичном сбое**: если клиент успешно создан в 3x-ui, а `SaveChanges` в БД упал — - **Компенсация при частичном сбое**: если клиент успешно создан в 3x-ui, а `SaveChanges` в БД упал —
хендлер вызывает `RemoveClientAsync`, чтобы не оставить сироту в панели. хендлер вызывает `RemoveClientAsync`, чтобы не оставить сироту в панели.
- **Защита от гонок на квоте — `pg_advisory_xact_lock`**, не оптимистичная блокировка: перед проверкой - **Защита от гонок на квоте — `pg_advisory_xact_lock`**, не оптимистичная блокировка: перед проверкой
+8 -23
View File
@@ -4,8 +4,7 @@
`AppUser`/`AppRole` — часть Identity (живут в `Infrastructure`, т.к. расширяют `IdentityUser<Guid>`/ `AppUser`/`AppRole` — часть Identity (живут в `Infrastructure`, т.к. расширяют `IdentityUser<Guid>`/
`IdentityRole<Guid>`); чистый `PnvPanel.Domain` ссылается на пользователя/роль только по `Guid`. `IdentityRole<Guid>`); чистый `PnvPanel.Domain` ссылается на пользователя/роль только по `Guid`.
Ниже — то, что реально реализовано и работает. Тарифы `Plan` и лимиты трафика на конфиг Тарифы `Plan` и лимиты трафика на конфиг (`TrafficLimit`) не реализованы — единственная квота:
(`TrafficLimit`) были в первоначальном плане, но остались в backlog — квота в MVP только одна:
число активных конфигов на роль (`AppRole.MaxConfigs`). число активных конфигов на роль (`AppRole.MaxConfigs`).
## Диаграмма связей ## Диаграмма связей
@@ -84,7 +83,7 @@ ClientApp (каталог приложений-клиен
| `DeviceLimit` | `int` | Лимит одновременных устройств/IP (0 = без лимита); задаёт юзер → `limitIp` в 3x-ui | | `DeviceLimit` | `int` | Лимит одновременных устройств/IP (0 = без лимита); задаёт юзер → `limitIp` в 3x-ui |
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui (только для отображения — лимит трафика не применяется) | | `UsedUpBytes` | `long` | Синхронизируется из 3x-ui (только для отображения — лимит трафика не применяется) |
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui | | `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
| `ExpiresAt` | `DateTimeOffset?`| Зарезервировано на будущее — в MVP ничего его не выставляет, конфиг живёт бессрочно | | `ExpiresAt` | `DateTimeOffset?`| Зарезервировано, сейчас ничего его не выставляет конфиг живёт бессрочно |
| `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`| | `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`|
| `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` | | `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` |
| `LastSyncAt` | `DateTimeOffset?`| | | `LastSyncAt` | `DateTimeOffset?`| |
@@ -109,22 +108,9 @@ ClientApp (каталог приложений-клиен
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`). - Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли). - Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
> **Не реализовано в MVP**: лимиты трафика и автоматическое истечение срока конфига. `ExpiresAt` > Лимиты трафика и автоматическое истечение срока конфига не реализованы. `ExpiresAt` никогда не
> никогда не выставляется, `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда > выставляется; `ConfigStatus.LimitReached` в значении enum есть, но код в него никогда не переводит
> не переводит конфиг — оставлено на будущее (см. `Plan` ниже и Backlog в [vision.md](vision.md)). > конфиг. Квота на число конфигов реализована через `AppRole.MaxConfigs` (см. [tech-stack.md](tech-stack.md)).
### Plan — тариф (backlog, не реализовано)
Планировался как шаблон лимитов трафика/срока для конфига — **квота на число конфигов уже
реализована через `AppRole.MaxConfigs`, это не Plan**. Сущности `Plan` в коде нет; таблица ниже —
эскиз на будущее, если/когда лимиты трафика/срока понадобятся.
| Поле | Тип | Заметки |
| ------------------ | ----------- | ------------------------------ |
| `Id` | `Guid` | PK |
| `Name` | `string` | |
| `TrafficLimitBytes`| `long` | 0 = безлимит |
| `DurationDays` | `int?` | Срок действия конфига |
| `IsActive` | `bool` | |
### TrafficSample — история трафика (для графиков) ### TrafficSample — история трафика (для графиков)
Точки потребления во времени; пишутся синхронизацией. Точки потребления во времени; пишутся синхронизацией.
@@ -137,8 +123,7 @@ ClientApp (каталог приложений-клиен
| `UpBytes` | `long` | Накопительно или дельта | | `UpBytes` | `long` | Накопительно или дельта |
| `DownBytes` | `long` | | | `DownBytes` | `long` | |
> **Решение**: обычная таблица PostgreSQL + **TTL** — фоновая чистка записей старше N дней > Обычная таблица PostgreSQL + **TTL** — фоновая чистка записей старше N дней (`TrafficRetentionService`).
> (`TrafficRetentionService`). TimescaleDB/агрегация — вне MVP.
### ClientApp — каталог приложений для подключения ### ClientApp — каталог приложений для подключения
Приложения-клиенты, которые админ рекомендует пользователям. На странице инструкций отображаются Приложения-клиенты, которые админ рекомендует пользователям. На странице инструкций отображаются
@@ -284,8 +269,8 @@ enum OsPlatform { IOS, Android, Windows, MacOS, Linux }
## Уведомления и аудит (без диспетчера доменных событий) ## Уведомления и аудит (без диспетчера доменных событий)
В `Domain` нет маркера `IDomainEvent` и диспетчера событий — упрощение относительно исходного плана. В `Domain` нет маркера `IDomainEvent` и диспетчера событий. CQRS-хендлеры сами вызывают порты
CQRS-хендлеры сами вызывают порты `IRealtimeNotifier` / `ITelegramNotifier` и пишут `AuditLog` `IRealtimeNotifier` / `ITelegramNotifier` и пишут `AuditLog`
напрямую, после того как изменение состояния сохранено. Так проще проследить, что именно произойдёт напрямую, после того как изменение состояния сохранено. Так проще проследить, что именно произойдёт
при вызове конкретной команды — не нужно искать обработчик события где-то ещё. при вызове конкретной команды — не нужно искать обработчик события где-то ещё.
+2 -3
View File
@@ -28,9 +28,8 @@ SPA на **React 19 + Vite + TypeScript**. Общается с бэком по R
| Линт | oxlint (не ESLint) | | Линт | oxlint (не ESLint) |
| Пакетный менеджер | pnpm | | Пакетный менеджер | pnpm |
Установлены, но **не используются в MVP**: `recharts` (админская статистика — карточки с цифрами, Установлены, но не используются: `recharts` (админская статистика — карточки с цифрами, без
без графиков), `@tanstack/react-table` (админские таблицы написаны руками, без TanStack Table). графиков), `@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 ## Backend
### Платформа: .NET 10 + ASP.NET Core Web API - **Платформа**: .NET 10, ASP.NET Core Web API (Minimal API).
Долгосрочная (LTS-класса) современная платформа, нативная поддержка Minimal API, rate limiting, - **Архитектура**: Clean Architecture, 4 проекта — `Domain / Application / Infrastructure / Api`.
health checks, DI. `ThreeXui.Net` таргетит `net10.0` — совпадение целевого фреймворка. - **CQRS**: собственный тонкий диспетчер (`ISender`), без MediatR. `ISender.Send()` резолвит
`ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через `IPipelineBehavior<,>`:
### Архитектура: Clean Architecture (4 проекта) `ValidationBehavior` (FluentValidation), `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция +
`Domain / Application / Infrastructure / Api`. Тестируемость, изоляция домена, заменяемость инфраструктуры. `SaveChangesAsync` на команду). Авторизация проверяется на уровне эндпоинта
Альтернативы: Vertical Slice (проще для мелких API, но хуже изолирует домен для растущего продукта) — (`RequireAuthorization(...)`), более тонкие проверки (владение, активация) — в хендлере.
можно комбинировать: слои + организация Application «по фичам». - **Валидация**: FluentValidation, подключается через `ValidationBehavior` (не для каждой команды —
только там, где есть что проверить помимо типов).
### CQRS: собственный тонкий диспетчер ✅ (зафиксировано и реализовано) - **Маппинг**: вручную, статический метод `XxxDto.FromDomain(entity)` на самом DTO.
**Решение принято**: свой `ISender` вместо MediatR (тот с v12 стал платным). `ISender.Send()` - **ORM**: EF Core 10 + Npgsql. Миграции, `IEntityTypeConfiguration`. Запросы-чтения — проекции в DTO
резолвит `ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через `IPipelineBehavior<,>`. (`AsNoTracking` + `Select`).
Реализованы три поведения: `ValidationBehavior` (FluentValidation), `LoggingBehavior`, - **БД**: PostgreSQL.
`UnitOfWorkBehavior` (транзакция + `SaveChangesAsync` на команду). Плюсы: нет лицензий и внешних - **Auth**: ASP.NET Core Identity + JWT (access, короткий TTL) + refresh (httpOnly cookie, ротация).
зависимостей, полный контроль. Отклонены: MediatR (лицензия), FastEndpoints/Wolverine (лишняя связанность/переписывание модели). - **RBAC**: динамические роли с квотой (`AppRole.MaxConfigs`). Доступ к инбаундам — по ролям
(`Inbound.AllowedRoles`). Квота на число конфигов — на роли, а не на тариф.
**Отличие от исходного плана**: отдельного диспетчера доменных событий (`IDomainEventHandler<T>`) в - **Активация пользователей**: `AppUser.IsActivated` + `ActivationRequest` (с комментарием).
итоге не заводили — оказалось, что для текущего размера проекта прямые вызовы `IRealtimeNotifier`/ Неактивированный не создаёт конфиги; решение принимает админ на сайте или в Telegram — одними и
`ITelegramNotifier` и запись `AuditLog` прямо в хендлере команды читаются проще, чем публикация теми же CQRS-командами.
события и поиск обработчика где-то ещё (см. [domain-model.md](domain-model.md#уведомления-и-аудит-без-диспетчера-доменных-событий)). - **Сидинг из env**: идемпотентный `DbInitializer` на старте — системные роли (`admin`/`user`),
Также нет отдельного `AuthorizationBehavior` — роль проверяется на уровне эндпоинта учётка админа и Telegram id админов. Пример — [`.env.example`](../.env.example).
(`RequireAuthorization(...)`), а более тонкие проверки (владение, активация) — в самом хендлере. - **Realtime**: SignalR — авто-транспорт (WebSocket→SSE→long-poll), группы/пользователи,
JWT-авторизация хабов.
### Валидация: FluentValidation - **Telegram-бот**: Telegram.Bot, хостится в процессе Api как `BackgroundService` (long polling) и
Декларативные валидаторы на команды/запросы, подключаются через `ValidationBehavior`. Заводится не вызывает те же CQRS-хендлеры, что и REST. Passwordless-вход выпускает те же JWT/refresh, что и веб.
для каждой команды — только там, где есть что проверить помимo типов (например, у команд без Детали — [telegram-bot.md](telegram-bot.md).
пользовательского ввода валидатора нет). - **Фоновые задачи**: `BackgroundService` + `PeriodicTimer`, без внешних зависимостей.
- **Ошибки**: собственный `Result<T>` вместо исключений для управляемых сценариев; исключения — только
### Маппинг: вручную, без Mapster для действительно исключительного.
В исходном плане был Mapster — на практике для такого числа полей ручной статический метод - **Логирование**: Serilog (`Serilog.AspNetCore`), `UseSerilogRequestLogging()` +
`XxxDto.FromDomain(entity)` на самом DTO читается не хуже конфига маппера и не добавляет `Enrich.FromLogContext()`. Секреты (пароли, JWT, `BotToken`) в логи не попадают.
зависимость. `Mapster` в проект так и не попал. - **API-документация**: нативный OpenAPI (`Microsoft.AspNetCore.OpenApi`) + Scalar UI, без Swashbuckle.
`/openapi/v1.json` используется фронтом для `pnpm gen:api` (openapi-typescript). `/scalar` — UI.
### ORM: EF Core 10 + Npgsql - **Тесты**: xUnit + NSubstitute + Testcontainers (юнит-тесты домена/хендлеров, интеграционные — с
Миграции, LINQ, `IEntityTypeConfiguration`. Провайдер PostgreSQL — Npgsql. реальным PostgreSQL через `Testcontainers.PostgreSql` + `WebApplicationFactory<Program>`).
Запросы-чтения — проекции в 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>`).
## Frontend ## Frontend
### React 19 + Vite + TypeScript - **React 19 + Vite + TypeScript** — SPA, без SSR.
Максимальная экосистема, быстрый dev-сервер и сборка Vite, строгая типизация. SPA (не SSR) — - **Данные с сервера**: TanStack Query — кэш, инвалидация, фоновые рефетчи, статусы загрузки/ошибок.
для внутренней панели SSR избыточен и усложняет деплой рядом с C# API. - **Роутинг**: TanStack Router — типобезопасный, интеграция с TanStack Query.
- **UI**: shadcn/ui + Tailwind CSS v4 (компоненты на Radix), тёмная/светлая тема. Иконки —
### Данные с сервера: TanStack Query `lucide-react`.
Кэш, инвалидация, фоновые рефетчи, статусы загрузки/ошибок. Идеально для CRUD-панели. - **Клиентский стейт**: Zustand — только для авторизации и темы; серверный стейт — в TanStack Query.
- **Формы**: react-hook-form + zod.
### Роутинг: TanStack Router - **Realtime**: `@microsoft/signalr` — подписки на события хаба обновляют кэш TanStack Query.
Типобезопасный роутинг, интеграция с TanStack Query. Альтернатива — React Router 7. - **Типы API**: `pnpm gen:api` гоняет `openapi-typescript` по `/openapi/v1.json` живого бэкенда →
`shared/api/schema.gen.ts`. Фичи импортируют типы из руками написанного `shared/api/types.ts`
### UI: shadcn/ui + Tailwind CSS v4 (см. [frontend.md](frontend.md)) — даёт нормальные generic (`PagedList<T>`) и понятные имена.
Копируемые в проект, полностью кастомизируемые компоненты (Radix под капотом), современный вид, - **QR-коды**: `qrcode.react`.
тёмная тема из коробки. Иконки — `lucide-react`. - **i18n**: react-i18next, языки RU + EN (RU по умолчанию). Тексты — через ключи, не хардкод строк.
### Клиентский стейт: 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`), не хардкод строк в компонентах.
## Инфраструктура ## Инфраструктура
### Упаковка: единый образ приложения + PostgreSQL Единый образ приложения (REST + SignalR + Telegram-бот + статика SPA) + отдельный контейнер PostgreSQL.
По требованию — **один контейнер на всё приложение** (REST + SignalR + Telegram-бот + статика SPA)
и отдельный контейнер БД.
- **Multi-stage Dockerfile**: (1) `node` собирает фронт → `dist/`; (2) `dotnet sdk` публикует Api и - **Multi-stage Dockerfile**: (1) `node` собирает фронт → `dist/`; (2) `dotnet sdk` публикует Api и
копирует статику в `wwwroot`; (3) `aspnet` runtime запускает Api. Api раздаёт SPA (`UseStaticFiles` копирует статику в `wwwroot`; (3) `aspnet` runtime запускает Api. Api раздаёт SPA (`UseStaticFiles`
+ fallback на `index.html`), фронт и бек — один origin. + fallback на `index.html`), фронт и бек — один origin.
- **docker-compose**: `app` (единый образ) + `db` (PostgreSQL) с томом. - **docker-compose**: `app` (единый образ) + `db` (PostgreSQL) с томом.
- Почему не отдельный nginx: единый origin упрощает CORS/куки/деплой и укладывается в требование - Отдельного nginx для статики нет — единый origin упрощает CORS/куки/деплой.
«фронт+бек в одном контейнере». - **TLS — внешний**: HTTPS терминирует внешний прокси/шлюз (nginx/Traefik/cloud LB) вне compose;
- **TLS — внешний** (решение): HTTPS терминирует внешний прокси/шлюз (nginx/Traefik/cloud LB) вне `app` отдаёт HTTP и доверяет `X-Forwarded-*` через `ForwardedHeaders`.
compose; `app` отдаёт HTTP и доверяет `X-Forwarded-*` через `ForwardedHeaders`. Свой nginx/Caddy не вводим. - **Миграции** — применяются автоматически на старте приложения.
- **Миграции** — авто на старте приложения (MVP). - **CI** — GitHub Actions: `dotnet build`/`test`, `pnpm build`/`lint`/`typecheck`. Без деплоя.
- **CI** — GitHub Actions, **только сборка/тесты**: `dotnet build`/`test`, `pnpm build`/`lint`/`typecheck`. - **Пакетный менеджер фронта**: pnpm.
Публикация образа и деплой — вручную/позже (в MVP не автоматизируем).
- **Пакетный менеджер фронта**: 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)) |
| # | Вопрос | Решение | Также реализовано: Identity lockout по неудачным входам; проверка квоты конфигов под
| - | ------------------------------ | ------------------------------------------------------------------- | `pg_advisory_xact_lock`; схема `ClientEmail = pnv_{userIdShort}_{rand}`. Явного анти-CSRF токена на
| 1 | CQRS-медиатор | **Собственный тонкий диспетчер** (не MediatR) | refresh-cookie нет — обоснование в [architecture.md](architecture.md#безопасность) (`SameSite=Strict`
| 2 | Ролей у пользователя | **Ровно одна роль** (квота = `MaxConfigs` роли) | + `HttpOnly` достаточно при мутациях только по Bearer-токену). Удаление ноды с активными конфигами
| 3 | Секреты нод | **ASP.NET Core Data Protection** (шифрование at-rest, key-ring на томе) | **не блокируется** — известный пробел: `DeleteNodeCommandHandler` каскадно удаляет инбаунды ноды без
| 4 | Тарифы `Plan` в MVP | **Backlog** — в MVP конфиги без лимитов трафика/срока | проверки существующих `VpnConfig`.
| 5 | i18n | **RU + EN** с первого дня (react-i18next) |
| 6 | Telegram-транспорт | **Long polling** |
| 7 | Регистрация через Telegram | **Только привязка** существующего аккаунта (signup из бота — backlog) |
| 8 | История трафика `TrafficSample`| **Простая таблица PostgreSQL + TTL** (фоновая чистка старше N дней) |
| 9 | Логирование | **Serilog** (Console + rolling file) |
### Продуктовые решения (поведение) Email/SMTP в проекте не используются (вход по username, восстановление — через Telegram/админа).
| Тема | Решение |
| ------------------------ | ------------------------------------------------------------------------------- |
| Вход | **По 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/админа).
+3 -3
View File
@@ -34,7 +34,7 @@ Telegram-бот — **второй канал доставки** (presentation-
(пусто — кнопки нет). Слэш-команды `/configs`/`/unlink` продолжают работать как раньше — кнопки лишь (пусто — кнопки нет). Слэш-команды `/configs`/`/unlink` продолжают работать как раньше — кнопки лишь
вызывают те же обработчики через callback (`menu:configs`/`menu:unlink`/`menu:back`). вызывают те же обработчики через callback (`menu:configs`/`menu:unlink`/`menu:back`).
**Не реализовано / backlog:** **Не реализовано:**
- QR-картинкой и агрегированная подписка в самом боте (только текстовая ссылка на конфиг по кнопке). - QR-картинкой и агрегированная подписка в самом боте (только текстовая ссылка на конфиг по кнопке).
- Отдельная команда `/resetpassword` с одноразовой ссылкой — восстановление пароля сейчас идёт - Отдельная команда `/resetpassword` с одноразовой ссылкой — восстановление пароля сейчас идёт
только через обычный passwordless-вход (`/start login_<n>`) + смену пароля в настройках на сайте. только через обычный passwordless-вход (`/start login_<n>`) + смену пароля в настройках на сайте.
@@ -224,5 +224,5 @@ Username бота для deepLink (кнопка «Привязать Telegram»/
сидируется в БД и не связан с учёткой сид-админа (`AdminSeed:*`), это независимый список. сидируется в БД и не связан с учёткой сид-админа (`AdminSeed:*`), это независимый список.
Сообщения бота **не локализованы** по языку пользователя — все тексты на русском независимо от языка Сообщения бота **не локализованы** по языку пользователя — все тексты на русском независимо от языка
интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом интерфейса на сайте (в отличие от веба, где RU/EN переключаются). Синхронизация языка бота с вебом
не реализована, backlog. не реализована.
+5 -5
View File
@@ -75,8 +75,8 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
### U2. Пользователь следит за трафиком ### U2. Пользователь следит за трафиком
- Фоновая синхронизация тянет трафик из 3x-ui; изменения приходят в UI через SignalR (без перезагрузки). - Фоновая синхронизация тянет трафик из 3x-ui; изменения приходят в UI через SignalR (без перезагрузки).
- **В MVP это только отображение**: лимиты по трафику/сроку конфига не реализованы — единственная - Это только отображение: лимиты по трафику/сроку конфига не реализованы — единственная квота —
квота — число активных конфигов на роль. Конфиг живёт, пока его явно не отзовут. число активных конфигов на роль. Конфиг живёт, пока его явно не отзовут.
### A1. Админ подключает ноду и публикует инбаунды ### A1. Админ подключает ноду и публикует инбаунды
1. Вводит адрес панели 3x-ui, логин/пароль (шифруются при хранении). 1. Вводит адрес панели 3x-ui, логин/пароль (шифруются при хранении).
@@ -97,9 +97,9 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
3. В следующий раз на сайте выбирает «Войти через Telegram» → подтверждает вход в боте → входит без пароля. 3. В следующий раз на сайте выбирает «Войти через Telegram» → подтверждает вход в боте → входит без пароля.
4. В боте может смотреть свои конфиги и открывать сайт. 4. В боте может смотреть свои конфиги и открывать сайт.
## Границы MVP ## Функциональность
**В MVP входит:** **Реализовано:**
- Регистрация/вход (JWT + Identity); сид админа из env. - Регистрация/вход (JWT + Identity); сид админа из env.
- Динамические роли с квотой конфигов (сид `admin`/`user`); создание ролей и назначение админом. - Динамические роли с квотой конфигов (сид `admin`/`user`); создание ролей и назначение админом.
- Активация пользователей по запросу с комментарием (одобрение на сайте и в Telegram). - Активация пользователей по запросу с комментарием (одобрение на сайте и в Telegram).
@@ -112,7 +112,7 @@ PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует
- Страница инструкций по подключению + каталог приложений по ОС (админ ведёт, юзер видит сгруппировано). - Страница инструкций по подключению + каталог приложений по ОС (админ ведёт, юзер видит сгруппировано).
- Единый Docker-образ (фронт+бек) + PostgreSQL в docker-compose. - Единый Docker-образ (фронт+бек) + PostgreSQL в docker-compose.
**За рамками MVP (backlog):** **Не реализовано:**
- Тарифы/биллинг/платежи. - Тарифы/биллинг/платежи.
- Многоуровневые квоты, автопродление, промокоды. - Многоуровневые квоты, автопродление, промокоды.
- Балансировка нагрузки между нодами, автоскейл. - Балансировка нагрузки между нодами, автоскейл.