Update .gitignore to include local environment files and expand README with project details, tech stack, documentation links, and project status.

This commit is contained in:
Leonid Pershin
2026-07-01 18:37:54 +03:00
parent 3b364cf8c4
commit d8930409fe
14 changed files with 1780 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
# PnvPanel — Документация
Индекс проектной документации. Читать в этом порядке для погружения:
1. **[Product Vision & Scope](vision.md)** — продукт, роли, пользовательские сценарии, границы MVP.
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)** — принятые решения по технологиям и их обоснование.
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) и порядок реализации.
## Принятые решения
Ключевые развилки закрыты (полная таблица — в [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.
+181
View File
@@ -0,0 +1,181 @@
# API Design
REST поверх HTTP/JSON, авторизация — `Authorization: Bearer <access-token>` (кроме публичных).
Ошибки — `application/problem+json` (`ProblemDetails`). Пагинация — `?page=&pageSize=`,
ответ `PagedList<T>` (`items`, `total`, `page`, `pageSize`). Все даты — ISO-8601 UTC.
Базовый префикс: `/api` (**без версионирования в MVP** — единый фронт+бек; версии введём при
необходимости). Ниже — контракт MVP (может уточняться при реализации).
## Auth
| Метод | Путь | Роль | Описание |
| ----- | --------------------------- | ------ | ---------------------------------------------------- |
| POST | `/api/auth/register` | — | Регистрация `{ username, password }` |
| POST | `/api/auth/login` | — | Вход `{ username, password }` → access (body) + refresh (httpOnly cookie) |
| POST | `/api/auth/refresh` | — | Обновление access по refresh-cookie (ротация) |
| POST | `/api/auth/logout` | user | Отзыв refresh-токена |
| POST | `/api/auth/change-password` | user | Смена пароля `{ currentPassword, newPassword }` |
| GET | `/api/auth/me` | user | Текущий профиль + роль + `isActivated` + `telegramLinked` |
| DELETE| `/api/auth/me` | user | Самоудаление аккаунта (отзыв всех конфигов + удаление данных; аудит анонимизируется) |
> **Вход по username.** Email в системе не используется. Забыт пароль:
> при привязанном Telegram — восстановление через бота; иначе — сброс админом (см. Admin).
## Auth — Telegram (привязка и passwordless-вход)
| Метод | Путь | Роль | Описание |
| ----- | --------------------------------------------- | ---- | -------------------------------------------------------------- |
| POST | `/api/auth/telegram/link-token` | user | Создать токен привязки → `{ deepLink, qr, expiresAt }` |
| POST | `/api/auth/telegram/unlink` | user | Отвязать Telegram от аккаунта |
| POST | `/api/auth/telegram/login-request` | — | Инициировать вход → `{ requestId, deepLink, qr, expiresAt }` |
| GET | `/api/auth/telegram/login-request/{id}` | — | Статус запроса; при `Approved` выдаёт access + refresh-cookie |
`GET …/login-request/{id}` (поллинг; альтернатива — событие SignalR) → варианты ответа:
```json
// ожидание
{ "status": "Pending" }
// подтверждено — выпуск токенов (refresh уходит в httpOnly cookie), запрос → Consumed
{ "status": "Approved", "accessToken": "…", "expiresAt": "…", "user": { "id": "…", "roles": ["User"] } }
// отклонено / истекло
{ "status": "Rejected" } // | "Expired"
```
> Сами апдейты Telegram (`/start`, кнопки) обрабатывает in-process бот (long polling), а не HTTP-эндпоинты.
> Контракты команд бота — в [telegram-bot.md](telegram-bot.md).
## Configs (пользователь)
| Метод | Путь | Роль | Описание |
| ------ | --------------------------------- | ---- | ------------------------------------------------- |
| GET | `/api/inbounds/available` | user | Инбаунды, доступные роли пользователя (для выбора при создании) |
| GET | `/api/configs` | user | Список своих конфигов (пагинация) |
| POST | `/api/configs` | user | Создать конфиг `{ inboundId, label?, deviceLimit? }` (проверки: активирован, квота роли, доступ роли к инбаунду) |
| PATCH | `/api/configs/{id}` | user | Изменить `{ label?, deviceLimit? }` (deviceLimit → `limitIp` в 3x-ui) |
| GET | `/api/configs/{id}` | user | Детали конфига (метка, трафик, устройства, статус) |
| GET | `/api/configs/{id}/link` | user | Connection string + subscriptionUrl + QR-payload |
| POST | `/api/configs/{id}/rotate` | user | Перевыпустить конфиг (новый UUID/ссылка; квоту не тратит) |
| DELETE | `/api/configs/{id}` | user | Отозвать конфиг (удаляет клиента в 3x-ui) |
| GET | `/api/subscription` | user | URL агрегированной подписки пользователя (все активные конфиги) |
`POST /api/configs``201 Created`:
```json
{
"id": "…", "protocol": "Vless", "location": "DE",
"link": "vless://…", "subscriptionUrl": "https://…/sub/…",
"trafficLimitBytes": 53687091200, "expiresAt": "2026-08-01T00:00:00Z",
"status": "Active"
}
```
## Apps — каталог приложений
| Метод | Путь | Роль | Описание |
| ------ | -------------------------- | ----- | ---------------------------------------------------- |
| GET | `/api/apps` | user | Включённые приложения, **сгруппированы по ОС** (для страницы инструкций) |
| GET | `/api/admin/apps` | admin | Все приложения (вкл. выключенные) |
| POST | `/api/admin/apps` | admin | Добавить `{ name, downloadUrl, operatingSystem, description?, iconUrl?, sortOrder? }` |
| PUT | `/api/admin/apps/{id}` | admin | Изменить приложение (в т.ч. `isEnabled`) |
| DELETE | `/api/admin/apps/{id}` | admin | Удалить приложение |
`GET /api/apps` → пример:
```json
{
"Android": [ { "id": "…", "name": "v2rayNG", "downloadUrl": "https://…", "iconUrl": null } ],
"iOS": [ { "id": "…", "name": "Hiddify", "downloadUrl": "https://…", "iconUrl": null } ]
}
```
## Activation (пользователь)
| Метод | Путь | Роль | Описание |
| ----- | --------------------------- | ---- | ------------------------------------------------------ |
| GET | `/api/activation/status` | user | Статус активации + текущий `Pending`-запрос (если есть)|
| POST | `/api/activation/request` | user | Запросить активацию `{ comment? }` (напр. «я Никита») |
`GET /api/configs` для неактивированного пользователя вернёт пустой список; `POST /api/configs`
до активации → `403` (или `409` с кодом `NotActivated`).
## Admin — Activation, Roles
| Метод | Путь | Роль | Описание |
| ------ | ----------------------------------------------- | ----- | --------------------------------------------------- |
| GET | `/api/admin/activation-requests` | admin | Список запросов активации (фильтр по статусу) |
| POST | `/api/admin/activation-requests/{id}/approve` | admin | Одобрить → пользователь активирован |
| POST | `/api/admin/activation-requests/{id}/reject` | admin | Отклонить `{ reason? }` |
| GET | `/api/admin/roles` | admin | Список ролей с квотами |
| POST | `/api/admin/roles` | admin | Создать роль `{ name, maxConfigs }` |
| PUT | `/api/admin/roles/{id}` | admin | Изменить роль (напр. `maxConfigs`) |
| DELETE | `/api/admin/roles/{id}` | admin | Удалить роль (нельзя системные `admin`/`user`) |
| PATCH | `/api/admin/users/{id}/role` | admin | Сменить роль пользователю `{ roleId }` (ровно одна) |
| PATCH | `/api/admin/users/{id}/activation` | admin | Активировать/деактивировать напрямую `{ isActivated }`|
## Admin — Nodes
| Метод | Путь | Роль | Описание |
| ------ | ----------------------------- | ----- | ----------------------------------------- |
| GET | `/api/admin/nodes` | admin | Список нод + статусы |
| POST | `/api/admin/nodes` | admin | Подключить ноду `{ name, baseAddress, username, password, location }` |
| PUT | `/api/admin/nodes/{id}` | admin | Изменить ноду (в т.ч. `isEnabled`) |
| DELETE | `/api/admin/nodes/{id}` | admin | Удалить ноду |
| POST | `/api/admin/nodes/{id}/sync` | admin | Пересинхронизировать inbounds с 3x-ui |
| POST | `/api/admin/nodes/{id}/probe` | admin | Проверить доступность |
## Admin — Inbounds
| Метод | Путь | Роль | Описание |
| ----- | -------------------------------------- | ----- | ---------------------------------------- |
| GET | `/api/admin/inbounds` | admin | Список inbounds (по нодам) + `allowedRoleIds`, `displayName` |
| PUT | `/api/admin/inbounds/{id}/publish` | admin | Опубликовать/снять `{ isPublished, displayName?, allowedRoleIds[], maxClients? }` |
## Admin — Users & Stats
| Метод | Путь | Роль | Описание |
| ----- | --------------------------------- | ----- | ----------------------------------------- |
| GET | `/api/admin/users` | admin | Пользователи (пагинация, поиск) |
| PATCH | `/api/admin/users/{id}/block` | admin | Блокировать/разблокировать `{ isBlocked }` (при блоке — отключить конфиги в 3x-ui) |
| POST | `/api/admin/users/{id}/reset-password` | admin | Сбросить пароль пользователю без привязки Telegram (выдать временный/задать новый) |
| GET | `/api/admin/users/{id}/configs` | admin | Конфиги пользователя |
| DELETE| `/api/admin/configs/{id}` | admin | Принудительно отозвать любой конфиг |
| GET | `/api/admin/stats` | admin | Сводная статистика (пользователи, конфиги, трафик) |
| GET | `/api/admin/audit` | admin | Журнал действий (`AuditLog`, пагинация, фильтры) |
## Public — Subscription
| Метод | Путь | Роль | Описание |
| ----- | ----------------- | ---- | ---------------------------------------------------------------- |
| GET | `/sub/{token}` | — | Подписка (base64-список ссылок). Токен — либо `AppUser.SubscriptionToken` (**все активные конфиги юзера**), либо `VpnConfig.SubscriptionToken` (**один конфиг**). Без `/api`. |
Rate-limited; отключённые/отозванные конфиги в выдачу не попадают; неизвестный/погашенный токен → 404.
Ответ отдаёт заголовок **`Subscription-Userinfo`** (`upload`/`download`/`total`/`expire`) — клиенты
(v2rayN/Nekoray и т.п.) показывают остаток трафика/срок. Также `profile-update-interval`.
## SignalR — Hub `/hubs/panel`
Авторизация — тем же JWT (query `access_token` или заголовок). Группы: `user:{userId}`, `admins`.
### Server → Client
| Событие | Payload | Кому |
| ---------------------- | ------------------------------------------------------------- | ------------ |
| `configTrafficUpdated` | `{ configId, usedUpBytes, usedDownBytes, limitBytes }` | владельцу |
| `configStatusChanged` | `{ configId, status }` | владельцу |
| `nodeStatusChanged` | `{ nodeId, status, lastSyncAt }` | `admins` |
| `activationRequested` | `{ requestId, userId, username, comment, createdAt }` | `admins` |
| `userActivated` | `{ userId }` | владельцу |
### Client → Server
MVP — клиент только слушает (группировка по пользователю на сервере при подключении по `UserId` из JWT).
## Коды ошибок
| Код | Когда |
| --- | -------------------------------------------------- |
| 400 | Ошибка валидации (`errors` в ProblemDetails) |
| 401 | Нет/просрочен токен |
| 403 | Нет прав (роль/владение/не активирован/роль без доступа к инбаунду) |
| 404 | Ресурс не найден |
| 409 | Конфликт домена (превышена квота роли, дубликат, уже есть Pending-запрос активации) |
| 422 | Нарушение инварианта домена |
| 429 | Rate limit |
| 502 | Ошибка/недоступность ноды 3x-ui (при необходимости)|
+243
View File
@@ -0,0 +1,243 @@
# Architecture
## Обзор
PnvPanel — backend на **ASP.NET Core (.NET 10)** по принципам **Clean Architecture** с **CQRS**,
и SPA-фронтенд на **React + Vite**. Backend хранит проекцию домена в **PostgreSQL** и
оркестрирует панели **3x-ui** через библиотеку **ThreeXui.Net**. Живые обновления — по **SignalR**.
```
┌──────────────────────────────────────────────────────────────────────────┐
│ React SPA (Vite + TS) │
│ TanStack Query/Router · shadcn/ui · @microsoft/signalr · zod │
└───────────────┬───────────────────────────────┬──────────────────────────┘
│ REST (JSON, JWT Bearer) │ WebSocket (SignalR)
┌───────────────▼───────────────────────────────▼──────────────────────────┐
│ PnvPanel.Api (Presentation) │
│ Minimal API endpoints · SignalR Hubs · Middleware · DI composition root │
└───────────────┬────────────────────────────────────────────────────────── ┘
│ ICommand / IQuery (свой диспетчер)
┌───────────────▼──────────────────────────────────────────────────────────┐
│ PnvPanel.Application │
│ Command/Query handlers · Validators · DTOs · Ports (interfaces) · │
│ Pipeline behaviors · Result<T> │
└───────────────┬───────────────────────────────┬──────────────────────────┘
│ implements ports │ uses
┌───────────────▼───────────────┐ ┌────────────▼──────────────────────────┐
│ PnvPanel.Infrastructure │ │ PnvPanel.Domain │
│ EF Core (Npgsql) · Identity · │ │ Entities · Value Objects · Domain │
│ JWT · XuiPanelGateway · │◄──┤ Events · Enums · Domain Exceptions │
│ Background sync · SignalR push│ │ (no external dependencies) │
└───────────────┬────────────────┘ └───────────────────────────────────────┘
┌───────────▼──────────┐ ┌──────────────────────────┐
│ PostgreSQL │ │ 3x-ui panels (nodes) │
│ (Npgsql / EF Core) │ │ via ThreeXui.Net (HTTP) │
└──────────────────────┘ └──────────────────────────┘
```
## Слои (Clean Architecture)
Зависимости направлены **внутрь**: `Api → Infrastructure → Application → Domain`.
Внутренние слои не знают о внешних. Инверсия зависимостей — через интерфейсы (порты) в
`Application`, реализуемые в `Infrastructure`.
### 1. `PnvPanel.Domain`
Ядро без внешних зависимостей (маркерный интерфейс доменных событий `IDomainEvent` — свой, в `Domain/Common`).
- **Entities**: `Node`, `Inbound`, `VpnConfig`, `Plan` (см. [domain-model.md](domain-model.md)).
- **Value Objects**: `TrafficLimit`, `NodeCredentials`, `ConnectionLink` и т.п.
- **Enums**: `VpnProtocol`, `ConfigStatus`, `NodeStatus`.
- **Domain Events**: `VpnConfigCreated`, `VpnConfigRevoked`, `TrafficLimitReached`, `NodeWentOffline`.
- **Domain Exceptions**: `DomainException` и специализированные (`ConfigQuotaExceededException`).
- Инварианты и бизнес-правила инкапсулированы в сущностях (rich domain model), а не в хендлерах.
> `AppUser` (Identity) живёт в `Infrastructure` (зависит от `IdentityUser`), а домен ссылается
> на пользователя по `UserId` (Guid), чтобы не тащить Identity в ядро.
### 2. `PnvPanel.Application`
Сценарии приложения через CQRS.
- **Commands / Queries** + их **Handlers** (`ICommandHandler<,>` / `IQueryHandler<,>` — свои интерфейсы).
- **Ports (интерфейсы)**: `IAppDbContext`, `IXuiPanelGateway`, `ICurrentUser`, `IJwtTokenService`,
`ISecretProtector`, `IRealtimeNotifier`, `IDateTime`.
- **Validators**: FluentValidation на каждую команду/запрос.
- **DTOs** и профили маппинга (Mapster).
- **Pipeline behaviors**: `ValidationBehavior`, `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция на команду), `AuthorizationBehavior`.
- **Result<T>**: явная модель успеха/ошибки вместо исключений для управляемых сценариев.
### 3. `PnvPanel.Infrastructure`
Технические детали и реализации портов.
- **Persistence**: `AppDbContext : IdentityDbContext<AppUser, AppRole, Guid>`, реализует `IAppDbContext`;
`IEntityTypeConfiguration<T>` для маппингов; миграции EF Core; репозитории только там, где нужны
(в основном хендлеры работают через `IAppDbContext` напрямую).
- **Identity & Auth**: ASP.NET Core Identity, `JwtTokenService` (access + refresh), хранение refresh-токенов.
- **3x-ui интеграция**: `XuiPanelGateway : IXuiPanelGateway` поверх `ThreeXui.Net`; фабрика клиентов per-node (см. ниже).
- **Realtime**: `SignalRRealtimeNotifier : IRealtimeNotifier` (пуш в хабы).
- **Background jobs**: `TrafficSyncService`, `NodeHealthCheckService` (`BackgroundService` + `PeriodicTimer`).
- **Secrets**: `DataProtectionSecretProtector : ISecretProtector` (шифрование паролей нод at-rest).
### 4. `PnvPanel.Api` (Presentation)
Композиционный корень и транспорт.
- **Minimal API** эндпоинты, сгруппированные по фичам (`MapAuthEndpoints`, `MapConfigEndpoints`, `MapAdminEndpoints`).
- **SignalR Hubs**: `PanelHub`.
- **Telegram-бот**: `TelegramBotHostedService` + хендлеры апдейтов в `Telegram/` (см. отдельный раздел).
- **Статика SPA**: раздача собранного фронта из `wwwroot` + SPA-fallback (единый контейнер).
- **Middleware**: обработка исключений → ProblemDetails, корреляция запросов, rate limiting.
- **DI**: `AddApplication()`, `AddInfrastructure()`, `AddApiServices()` — сборка всех слоёв.
- **OpenAPI**: Swashbuckle + Scalar UI; генерация схемы для codegen фронта.
## CQRS
- **Команды** меняют состояние, возвращают `Result` / `Result<T>`; выполняются в транзакции (UnitOfWorkBehavior).
- **Запросы** только читают; могут ходить в БД проекциями (`Select` в DTO) без загрузки сущностей целиком.
- Диспетчер — **собственный тонкий `ISender`**: резолвит хендлер команды/запроса из DI и прогоняет
через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — `ICommand<T>`,
`IQuery<T>`, `ICommandHandler<,>`, `IQueryHandler<,>`, `IPipelineBehavior<,>`.
Пример потока «создать конфиг»:
```
POST /api/configs
→ CreateVpnConfigCommand
→ ValidationBehavior (FluentValidation)
→ AuthorizationBehavior (роль/владение)
→ CreateVpnConfigHandler
· проверяет квоту пользователя (домен)
· IXuiPanelGateway.AddClientAsync(node, inbound, spec) // 3x-ui
· создаёт VpnConfig, сохраняет через IAppDbContext
· публикует VpnConfigCreated (domain event)
→ UnitOfWorkBehavior (commit)
→ 201 Created { id, link, subscriptionUrl }
```
## Интеграция с 3x-ui (ThreeXui.Net)
`ThreeXui.Net` конфигурируется на **один** `BaseAddress`, а у нас **несколько нод**. Поэтому:
- Порт `IXuiPanelGateway` инкапсулирует все операции с панелями и принимает `Node` (или его id):
`ListInboundsAsync`, `AddClientAsync`, `UpdateClientAsync`, `RemoveClientAsync`,
`GetClientTrafficAsync`, `BuildConnectionStringAsync`, `ProbeAsync`.
- `XuiPanelGateway` держит **фабрику/кэш `IXuiClient` per-node** (ключ — `NodeId`), создавая клиента
из расшифрованных `NodeCredentials` через `XuiHttpClientFactory`/`HttpClient`. Cookie-session и
авто-переавторизация на 401 обеспечиваются самой библиотекой.
- Ошибки панели маппятся в доменные/`Result`-ошибки; недоступная нода → `NodeStatus.Offline`, а не исключение наружу.
- Операции мутации по клиентам сериализуются per-inbound (библиотека уже использует мьютексы; на нашей стороне — идемпотентные команды).
- **Реконсиляция дрейфа**: 3x-ui — источник правды по клиентам. При синхронизации сверяем проекцию
с панелью: клиент удалён/изменён напрямую в 3x-ui → помечаем конфиг рассинхронизованным
(`Disabled`/флаг) и логируем; не «воскрешаем» молча. Наши записи о трафике/статусах обновляем из панели.
## Telegram-бот (presentation-адаптер)
Бот — **второй канал доставки** поверх той же Application-логики, что и REST API (не содержит
бизнес-правил). Полное описание — в [telegram-bot.md](telegram-bot.md). Ключевое для архитектуры:
- Хостится **в процессе Api** как `BackgroundService` (`TelegramBotHostedService`) — это условие
для упаковки «фронт+бек в одном контейнере». Транспорт — **long polling** (MVP), webhook — опция.
- Обращения к домену — только через собственный `ISender`, теми же командами/запросами, что и веб
(`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...). `Telegram.Bot`
не проникает в Application/Domain.
- **Passwordless-вход**: бот подтверждает `TelegramLoginRequest`, после чего Api выпускает те же
JWT/refresh, что и обычный логин (единые правила сессий). Требует предварительной привязки Telegram.
## Realtime (SignalR)
- Хаб `PanelHub` (`/hubs/panel`), авторизация по тому же JWT.
- **Группы**: `user:{userId}` (личные события), `admins` (события нод/системы).
- **События сервер→клиент** (см. [api-design.md](api-design.md)): `configTrafficUpdated`,
`configStatusChanged`, `nodeStatusChanged`.
- Пуш выполняет `SignalRRealtimeNotifier` (порт `IRealtimeNotifier`), вызываемый из хендлеров и
фоновых сервисов — Application-слой не зависит от SignalR напрямую.
## Фоновые задачи
- **TrafficSyncService** — периодически (`PeriodicTimer`) обходит активные ноды, тянет трафик по
клиентам, обновляет `VpnConfig`, пишет `TrafficSample` (для графиков), шлёт realtime-события,
помечает превышения (`TrafficLimitReached`).
- **NodeHealthCheckService** — health-probe нод, обновляет `NodeStatus`, оповещает `admins`.
- **TrafficRetentionService** — чистит `TrafficSample` старше N дней (TTL-ретеншн истории трафика).
- Для MVP — встроенный `BackgroundService`; при росте нагрузки — вынести в Hangfire/Quartz
(см. [tech-stack.md](tech-stack.md)).
## Сидирование и старт
При старте приложения выполняется идемпотентный сидинг (`DbInitializer`), управляемый переменными
окружения (см. [`.env.example`](../.env.example)):
- **Системные роли**: `admin` (без лимита конфигов) и `user` (`MaxConfigs = Roles__DefaultUserMaxConfigs`, по умолчанию 3).
- **Учётка администратора**: создаётся из `AdminSeed__Username` / `AdminSeed__Password`, если ещё нет;
сразу активирована и с ролью `admin`.
- **Telegram id админов** (`AdminSeed__TelegramUserIds`) — авторизуют админ-действия в боте и
адресуют уведомления (например, запросы на активацию).
Сидинг не перезаписывает существующие данные; смена пароля админа после первого старта — через приложение.
## RBAC — динамические роли и активация
- `AppRole` расширяет `IdentityRole<Guid>` полем `MaxConfigs`. **У пользователя ровно одна роль**;
квота = `MaxConfigs` его роли (`admin` — без лимита). Админ создаёт роли и меняет роль пользователя.
- **Активация**: `AppUser.IsActivated`; `ActivationRequest` (с комментарием) обрабатывается админом
на сайте (`Approve/Reject`-команды) или в Telegram. `ApproveActivation` ставит `IsActivated = true`
и шлёт realtime-пуш пользователю.
- **Ролевой доступ к инбаундам**: `Inbound.AllowedRoles` (M:N с `AppRole`); при создании конфига
доменный инвариант проверяет активацию, квоту и пересечение роли пользователя с `AllowedRoles`.
- **Понижение роли — грандфазеринг**: смена роли на меньшую квоту разрешена; существующие конфиги
сохраняются, создание новых блокируется до входа в квоту.
- **Блокировка пользователя**: `IsBlocked = true` → вход запрещён + все конфиги `Disabled` (отключение
клиентов в 3x-ui); разблокировка — обратная операция. Пишется в `AuditLog`.
- Уведомления: запросы активации → группа `admins` (SignalR) + Telegram (по `AdminSeed__TelegramUserIds`);
решения/блокировки → пользователю (SignalR + Telegram-DM, если привязан).
## Безопасность
- **AuthN**: ASP.NET Core Identity + JWT, **вход по `UserName`** (email в системе не используется).
Access-token — короткий TTL (in-memory на клиенте); refresh-token — httpOnly Secure cookie,
ротация при использовании, хранение хэша в БД.
- **Восстановление пароля**: через привязанный Telegram (passwordless-вход → смена пароля, либо
reset-флоу в боте); без привязки — сброс админом (`ResetUserPasswordCommand`). Email/SMTP не используются.
Смена пароля вошедшим — `POST /api/auth/change-password`.
- **AuthZ**: роли (`admin`/`user`/кастомные) + policy-based (`OwnsConfig`, `RequireAdmin`,
`RequireActivated`).
- **Секреты нод**: шифруются `ISecretProtector` (Data Protection) перед сохранением; в API/логи не попадают.
- **CSRF**: refresh-cookie — `SameSite=Strict/Lax`, `Secure`, `HttpOnly`; для cookie-based refresh —
анти-CSRF токен. Мутации — только по Bearer access-токену, не по cookie.
- **Brute-force**: Identity lockout по числу неудачных входов; rate-limit на `/auth/*`.
- **Rate limiting**: на `/auth/*`, создание/ротацию конфигов, запросы активации и Telegram (встроенный `RateLimiter` .NET).
- **Валидация входа**: FluentValidation + жёсткая типизация DTO; ошибки — единый `ProblemDetails`.
- **CORS**: в проде фронт и бек — один origin (CORS не нужен); в dev — строгий allowlist (`App__CorsOrigins`).
- **Аудит**: значимые действия (активация, блокировка, смена роли, отзыв, ноды/инбаунды) пишутся в
`AuditLog` (append-only) с источником `Web`/`Telegram`/`System`.
## Обработка ошибок
- Управляемые ошибки → `Result`/`Result<T>` → маппинг в HTTP-статус + `ProblemDetails`.
- Непредвиденные исключения → глобальный middleware → 500 + корреляция + структурный лог (без утечки деталей).
- Доменные исключения (нарушение инвариантов) → 409/422 с понятным сообщением.
## Развёртывание (единый контейнер приложения)
По требованию — **один контейнер на всё приложение** (фронт + бек + бот) и отдельный контейнер
PostgreSQL:
- **Единый образ**: ASP.NET Core (`PnvPanel.Api`) обслуживает REST (`/api`), SignalR (`/hubs`),
хостит Telegram-бота (long polling) **и** раздаёт статику React-SPA (`UseStaticFiles` +
SPA-fallback на `index.html` для клиентских маршрутов). Фронт и бек — один origin, база API — относительный `/api`.
- **Multi-stage Dockerfile**:
1. `node` — сборка фронта (`pnpm build`) → `dist/`.
2. `dotnet sdk``dotnet publish` Api; статика фронта копируется в `wwwroot`.
3. `dotnet aspnet` runtime — финальный образ запускает Api.
- **docker-compose**: сервис `app` (этот образ) + сервис `db` (PostgreSQL). Всё приложение — в `app`.
- Миграции применяются на старте (dev) / отдельным шагом (prod).
- Конфигурация через `appsettings.{Env}.json` + переменные окружения / secrets (строка подключения,
JWT-ключ, ключ шифрования секретов, `Telegram:BotToken`, `PublicSiteUrl`).
```
┌────────────────── docker-compose ──────────────────┐
│ app (единый образ) db (postgres) │
│ ├─ REST /api └─ том с данными │
│ ├─ SignalR /hubs/panel │
│ ├─ Telegram bot (long polling) │
│ └─ статика SPA (wwwroot, fallback → index.html) │
└─────────────────────────────────────────────────────┘
```
+115
View File
@@ -0,0 +1,115 @@
# Backend Conventions
## Структура решения
```
backend/
PnvPanel.sln
src/
PnvPanel.Domain/
Common/ # Entity, AggregateRoot, IDomainEvent, ValueObject base
Nodes/ # Node, NodeCredentials, NodeStatus, события
Inbounds/ # Inbound, VpnProtocol
Configs/ # VpnConfig, ConfigStatus, TrafficLimit, события
Plans/ # Plan
Exceptions/ # DomainException и наследники
PnvPanel.Application/
Common/
Behaviors/ # Validation, Logging, UnitOfWork, Authorization
Interfaces/ # IAppDbContext, IXuiPanelGateway, ICurrentUser, IRealtimeNotifier, ...
Messaging/ # ISender, ICommand<T>, IQuery<T>, ICommandHandler<,>, IQueryHandler<,>, IPipelineBehavior<,>
Models/ # Result<T>, Error, PagedList<T>
Mapping/ # Mapster-конфиги
Auth/ # Register/Login/Refresh (Commands, Handlers, Validators, DTOs)
Configs/ # CreateVpnConfig, EditVpnConfig, RotateVpnConfig, RevokeVpnConfig, GetMyConfigs, GetConfigLink, GetSubscription, ...
Nodes/ # RegisterNode, SyncNode, ListNodes, ...
Inbounds/ # PublishInbound, ListInbounds, ...
Admin/ # ListUsers, BlockUser/UnblockUser, ChangeUserRole, GetStats, Audit, ...
PnvPanel.Infrastructure/
Persistence/
AppDbContext.cs
Configurations/ # IEntityTypeConfiguration<T>
Migrations/
Identity/ # AppUser, AppRole, JwtTokenService, RefreshToken
Xui/ # XuiPanelGateway, XuiClientFactory (per-node)
Realtime/ # SignalRRealtimeNotifier
BackgroundJobs/ # TrafficSyncService, NodeHealthCheckService
Security/ # DataProtectionSecretProtector
DependencyInjection.cs
PnvPanel.Api/
Endpoints/ # AuthEndpoints, ConfigEndpoints, NodeEndpoints, AdminEndpoints, SubscriptionEndpoints
Hubs/ # PanelHub
Middleware/ # ExceptionHandling, RequestCorrelation
Extensions/ # AddApiServices, UseApiPipeline
Program.cs
appsettings*.json
tests/
PnvPanel.Domain.Tests/
PnvPanel.Application.Tests/
PnvPanel.Integration.Tests/ # Testcontainers PostgreSQL
```
Организация Application — **по фичам** (feature folders), внутри слоёв Clean Architecture.
## Именование
- Классы/методы/свойства — `PascalCase`; параметры/локальные — `camelCase`; приватные поля — `_camelCase`.
- Команды — `<Verb><Noun>Command` (`CreateVpnConfigCommand`), запросы — `<Get/List><Noun>Query`.
- Хендлеры — `<Command/Query>Handler`; валидаторы — `<Command/Query>Validator`.
- DTO — суффикс `Dto` (`VpnConfigDto`); ответы эндпоинтов — `Response`, тела запросов — `Request`.
- Async-методы — суффикс `Async`, всегда принимают `CancellationToken`.
- Один публичный тип на файл; имя файла = имя типа.
## Паттерны
- **Rich domain model**: инварианты в сущностях (приватные сеттеры, фабричные методы `Node.Create(...)`,
поведенческие методы `config.Revoke()`), а не анемичные DTO-сущности.
- **CQRS через собственный диспетчер**: хендлеры реализуют `ICommandHandler<TCommand,TResult>` /
`IQueryHandler<,>`; `ISender` резолвит их из DI и прогоняет через `IPipelineBehavior<,>`
(валидация, транзакция, логирование). Без внешних CQRS-библиотек.
- **Порты в Application, адаптеры в Infrastructure**: никакого `Npgsql`/`SignalR`/`ThreeXui.Net` в Application/Domain.
- **`IAppDbContext`** экспонирует `DbSet<>` и `SaveChangesAsync`; хендлеры пишут LINQ напрямую
(репозитории — только для сложной агрегатной логики).
- **Result-модель**: команды/запросы возвращают `Result<T>`; эндпоинт маппит в HTTP (`.Match(...)`).
- **Транзакция на команду**: `UnitOfWorkBehavior` оборачивает выполнение команды в транзакцию.
- **Валидация**: `ValidationBehavior` до хендлера; хендлер не проверяет формат ввода повторно.
- **Идемпотентность**: команды к 3x-ui устойчивы к повторам; при частичном сбое — компенсация
(создали клиента в панели, но упала БД → удалить клиента, вернуть ошибку).
- **Оптимистичная блокировка**: на изменяемых сущностях (нода, конфиг) — `xmin`/rowversion, чтобы
параллельные правки не затирали друг друга.
## Работа с 3x-ui
- Только через порт `IXuiPanelGateway`. Гейтвей принимает `Node`/`NodeId` и разруливает per-node клиента.
- Пароли нод расшифровываются `ISecretProtector` **внутри** Infrastructure, никогда не покидают слой.
- Сетевые ошибки/недоступность → `Result.Failure`/статус ноды, не «пробрасываем» наружу как 500.
## Async / Cancellation
- Всё I/O — асинхронно; пробрасывать `CancellationToken` до EF Core и HTTP-вызовов.
- Не блокировать (`.Result`/`.Wait()`).
## Ошибки и логирование
- Единый `ProblemDetails` для ошибок API; коды: 400 (валидация), 401/403 (auth), 404, 409 (конфликт домена), 422, 429 (rate limit), 500.
- Serilog со структурными полями (`UserId`, `NodeId`, `ConfigId`, `CorrelationId`); секреты не логировать.
## Тестирование
- **Domain.Tests** — инварианты и поведение сущностей, без моков.
- **Application.Tests** — хендлеры с подменёнными портами (NSubstitute), проверка веток `Result`.
- **Integration.Tests** — реальный PostgreSQL (Testcontainers), миграции, сквозные сценарии эндпоинтов;
3x-ui — мок гейтвея или фейковый HTTP-сервер.
- Именование тестов: `Method_Scenario_ExpectedResult`.
## Конфигурация
- `appsettings.json` + `appsettings.{Environment}.json` + env vars (перекрывают).
- Секреты (JWT-ключ, строка подключения, ключ шифрования) — user-secrets (dev) / env/secret-store (prod).
- Строго типизированные `IOptions<T>` для секций конфига; валидация опций на старте.
## Стиль и качество кода
- `.editorconfig` + анализаторы (`Microsoft.CodeAnalysis.NetAnalyzers`), nullable reference types **включены**.
- `dotnet format` в CI; предупреждения как ошибки для наших проектов.
- Комментарии — по необходимости (почему, а не что); публичные контракты портов документируем XML-doc.
+285
View File
@@ -0,0 +1,285 @@
# Domain Model
Домен — «rich model»: инварианты и переходы состояний живут в сущностях, а не в хендлерах.
`AppUser` — часть Identity (в `Infrastructure`); домен ссылается на пользователя по `UserId : Guid`.
## Диаграмма связей
```
AppUser (Identity) [+ IsActivated, TelegramUserId]
├─*───1─ AppRole (ровно одна роль; роль несёт квоту MaxConfigs)
├─1───*─ VpnConfig
│ *─┐
│ ├─1─ Inbound ─*─1─ Node
│ │ └─*───*─ AppRole (какие роли могут создавать конфиги в инбаунде)
│ └─*─ TrafficSample
├─0..1─* ActivationRequest (запрос активации у админа, с комментарием)
├─1───*─ TelegramLinkToken (короткоживущие токены привязки)
└─0..1─* TelegramLoginRequest (passwordless-вход)
Plan ─1───*─ VpnConfig (опционально; квота по числу конфигов — на роли, не на Plan)
AuditLog (append-only журнал действий; ссылается на ActorId/TargetId)
ClientApp (каталог приложений-клиентов; группируется по OperatingSystem)
```
## Сущности
### Node — VPN-сервер (панель 3x-ui)
Подключённая администратором панель 3x-ui.
| Поле | Тип | Заметки |
| ---------------- | --------------- | ------------------------------------------------------------ |
| `Id` | `Guid` | PK |
| `Name` | `string` | Отображаемое имя |
| `BaseAddress` | `Uri` | Напр. `https://panel.example.com:2053/` |
| `Credentials` | `NodeCredentials` (VO) | Логин + **зашифрованный** пароль (`ISecretProtector`) |
| `Location` | `string?` | Страна/город/тег для выбора пользователем |
| `Status` | `NodeStatus` | `Online` / `Offline` / `Unknown` |
| `IsEnabled` | `bool` | Выключена админом → скрыта из самообслуживания |
| `LastSyncAt` | `DateTimeOffset?` | Последняя успешная синхронизация |
| `CreatedAt` | `DateTimeOffset`| |
Инварианты: `BaseAddress` абсолютный; при `IsEnabled == false` или `Status == Offline` **новые**
конфиги на ноде запрещены, но **существующие не трогаем** (клиенты остаются в 3x-ui). Статус ноды
показываем пользователю как индикатор «состояние сервера».
### Inbound — прокси-inbound на ноде
Проекция inbound из 3x-ui; определяет протокол и параметры подключения.
| Поле | Тип | Заметки |
| ----------------- | ------------- | ---------------------------------------------------------- |
| `Id` | `Guid` | PK (внутренний) |
| `NodeId` | `Guid` | FK → Node |
| `RemoteInboundId` | `int` | Id inbound в 3x-ui |
| `Protocol` | `VpnProtocol` | `Vless` / `Vmess` / `Trojan` / `Shadowsocks` |
| `Remark` | `string` | Метка из 3x-ui |
| `Port` | `int` | |
| `IsPublished` | `bool` | Доступен ли для самообслуживания пользователями |
| `AllowedRoles` | `AppRole[]` (M:N) | Роли, которым разрешено создавать конфиги в этом инбаунде |
| `DisplayName` | `string?` | Витринное имя для пользователя, напр. «Германия (Trojan)» |
| `MaxClients` | `int?` | Лимит клиентов (null = без лимита) |
| `LastSyncAt` | `DateTimeOffset?` | |
Инварианты: конфиг можно создать только если `IsPublished && Node.IsEnabled`, **роль пользователя
входит в `AllowedRoles`**, и при заданном `MaxClients` он не достигнут. Публикация инбаунда админом
включает выбор `AllowedRoles` (напр. «Германия (Trojan)» → роли `user`, `vip`).
> **Пользователю показываем только `DisplayName` + протокол.** Адрес/хост ноды, `RemoteInboundId`,
> `Port` и прочие детали 3x-ui в пользовательские DTO не попадают (только в админские).
### VpnConfig — конфиг пользователя (клиент в 3x-ui)
Центральная сущность. Одна запись = один клиент внутри inbound + его привязка к пользователю.
| Поле | Тип | Заметки |
| ------------------ | ---------------- | -------------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `UserId` | `Guid` | FK → AppUser (владелец) |
| `InboundId` | `Guid` | FK → Inbound |
| `Label` | `string?` | Пользовательская метка («Мой телефон»); редактируется юзером |
| `ClientEmail` | `string` | Уникальный ключ клиента в 3x-ui; схема `pnv_{userIdShort}_{rand}` (уникален в рамках панели, виден владелец) |
| `ClientUuid` | `Guid` | UUID клиента (VLESS/VMess) |
| `Protocol` | `VpnProtocol` | Денормализовано с inbound |
| `DeviceLimit` | `int` | Лимит одновременных устройств/IP (0 = без лимита); задаёт юзер → `limitIp` в 3x-ui |
| `TrafficLimit` | `TrafficLimit` (VO) | Лимит в байтах (0 = безлимит) |
| `UsedUpBytes` | `long` | Синхронизируется из 3x-ui |
| `UsedDownBytes` | `long` | Синхронизируется из 3x-ui |
| `ExpiresAt` | `DateTimeOffset?`| null = бессрочно |
| `Status` | `ConfigStatus` | `Active` / `Disabled` / `Expired` / `LimitReached` / `Revoked`|
| `SubscriptionToken`| `string` | Секрет для публичного `/sub/{token}` |
| `LastSyncAt` | `DateTimeOffset?`| |
| `CreatedAt` | `DateTimeOffset` | |
Инварианты и переходы:
- Создаётся в статусе `Active`; поля клиента в 3x-ui и запись в БД создаются атомарно (компенсация при сбое).
- **Проверка квоты выполняется в транзакции с блокировкой** (иначе два параллельных создания пробьют лимит).
- `Revoke()` → удаляет клиента в 3x-ui, статус `Revoked` (запись остаётся для истории/аудита).
- `Rotate()` → перевыпуск: удаляет старого клиента в 3x-ui и создаёт нового (новый UUID/ссылка);
квоту **не тратит**. Для случая утечки ссылки.
- `Disable()`/`Enable()` → отключение/включение клиента в 3x-ui без удаления (используется при блокировке юзера).
- `Rename(label)` / `SetDeviceLimit(n)` → юзер меняет метку и лимит устройств (последнее синкается в `limitIp` 3x-ui).
- Синхронизация: если `Used ≥ TrafficLimit``LimitReached` (+ событие); если `now ≥ ExpiresAt``Expired`.
- **Создание разрешено только активированному пользователю** (`AppUser.IsActivated == true`).
- Число активных конфигов пользователя не может превышать **квоту его роли** (`AppRole.MaxConfigs`;
роль `admin` — без лимита). У пользователя ровно одна роль. См. `AppRole` ниже.
- Инбаунд должен быть доступен роли пользователя (`Inbound.AllowedRoles`).
- Разрешено несколько конфигов в одном инбаунде (ограничение — только общая квота роли).
### Plan — тариф (опционально, backlog)
Шаблон лимитов трафика/срока для конфига. **Квота на число конфигов — это `AppRole.MaxConfigs`,
а не Plan.** Plan остаётся опциональным механизмом для лимитов трафика/срока и в MVP не обязателен.
| Поле | Тип | Заметки |
| ------------------ | ----------- | ------------------------------ |
| `Id` | `Guid` | PK |
| `Name` | `string` | |
| `TrafficLimit` | `TrafficLimit` (VO) | Байты |
| `DurationDays` | `int?` | Срок действия конфига |
| `MaxConfigs` | `int` | Сколько конфигов даёт тариф |
| `IsActive` | `bool` | |
### TrafficSample — история трафика (для графиков)
Точки потребления во времени; пишутся синхронизацией.
| Поле | Тип | Заметки |
| ------------ | ---------------- | -------------------------- |
| `Id` | `long` | PK |
| `ConfigId` | `Guid` | FK → VpnConfig |
| `Timestamp` | `DateTimeOffset` | |
| `UpBytes` | `long` | Накопительно или дельта |
| `DownBytes` | `long` | |
> **Решение**: обычная таблица PostgreSQL + **TTL** — фоновая чистка записей старше N дней
> (`TrafficRetentionService`). TimescaleDB/агрегация — вне MVP.
### ClientApp — каталог приложений для подключения
Приложения-клиенты, которые админ рекомендует пользователям. На странице инструкций отображаются
**сгруппированными по ОС**; клик открывает ссылку на скачивание.
| Поле | Тип | Заметки |
| ----------------- | ------------- | --------------------------------------------------- |
| `Id` | `Guid` | PK |
| `Name` | `string` | Название, напр. «v2rayNG», «Hiddify», «NekoBox» |
| `DownloadUrl` | `Uri` | Ссылка на скачивание/стор |
| `OperatingSystem` | `OsPlatform` | `iOS` / `Android` / `Windows` / `MacOS` / `Linux` |
| `Description` | `string?` | Короткая подсказка (опц.) |
| `IconUrl` | `string?` | Иконка (опц.) |
| `SortOrder` | `int` | Порядок внутри группы ОС |
| `IsEnabled` | `bool` | Показывать пользователям |
Управляется админом (CRUD). Пользователю отдаётся только `IsEnabled`, сгруппировано по `OperatingSystem`.
### AuditLog — журнал действий
Аудит значимых действий (прежде всего админских) для расследований и прозрачности.
| Поле | Тип | Заметки |
| ------------ | ---------------- | -------------------------------------------------------------- |
| `Id` | `long` | PK |
| `ActorId` | `Guid?` | Кто выполнил (null — система/фон) |
| `Action` | `string` | Напр. `UserActivated`, `UserBlocked`, `RoleChanged`, `ConfigRevoked`, `NodeAdded`, `InboundPublished` |
| `TargetType` | `string` | Сущность (`User`/`Config`/`Node`/`Inbound`/`Role`) |
| `TargetId` | `string` | Идентификатор цели |
| `Metadata` | `jsonb` | Доп. детали (старое/новое значение, комментарий) |
| `Source` | `AuditSource` | `Web` / `Telegram` / `System` |
| `CreatedAt` | `DateTimeOffset` | |
Пишется из хендлеров (или обработчиков доменных событий), append-only.
### AppUser — расширения (Identity)
`AppUser` живёт в Identity (`Infrastructure`). **Логин — по `UserName`** (уникальный, обязательный).
**Email в системе не используется** — поле не заполняем/не требуем (стандартная колонка Identity
остаётся пустой). Помимо стандартных полей Identity:
| Поле | Тип | Заметки |
| ------------------- | ----------------- | ---------------------------------------------------- |
| `IsActivated` | `bool` | По умолчанию `false` при регистрации; активирует админ |
| `ActivatedAt` | `DateTimeOffset?` | Когда активирован |
| `ActivatedBy` | `Guid?` | Какой админ активировал |
| `IsBlocked` | `bool` | Блокировка админом: вход запрещён + все конфиги отключены в 3x-ui |
| `SubscriptionToken` | `string` | Секрет для **агрегированной** подписки `/sub/{token}` (все активные конфиги юзера) |
| `TelegramUserId` | `long?` | Id пользователя Telegram; **уникальный**; null до привязки |
| `TelegramUsername` | `string?` | @username на момент привязки (для отображения) |
| `TelegramLinkedAt` | `DateTimeOffset?` | Когда привязан |
Инварианты: один `TelegramUserId` ↔ один аккаунт (повторная привязка требует `/unlink`);
неактивированный пользователь не может создавать конфиги; при регистрации выдаётся роль `user`.
**Блокировка** (`IsBlocked = true`) переводит все конфиги в `Disabled` (отключение клиентов в 3x-ui);
разблокировка включает их обратно. У пользователя ровно одна роль.
**Восстановление пароля**: только через привязанный Telegram (passwordless-вход → смена пароля в
настройках, либо reset-флоу в боте). Если Telegram не привязан — пароль сбрасывает **админ**
(`ResetUserPasswordCommand`). Пока Telegram не привязан,
UI **настойчиво напоминает** привязать его (единственный self-service способ восстановления).
### AppRole — роль с квотой (Identity, динамическая)
Расширяет `IdentityRole<Guid>`. Роли **создаёт админ** и назначает пользователям; роль несёт квоту
на число конфигов.
| Поле | Тип | Заметки |
| ------------ | -------- | --------------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `Name` | `string` | Напр. `admin`, `user`, `vip` |
| `MaxConfigs` | `int` | Квота активных конфигов (для `admin` игнорируется — без лимита) |
| `IsSystem` | `bool` | Системная (`admin`, `user`) — нельзя удалить/переименовать |
Сидируются: `admin` (без лимита) и `user` (`MaxConfigs` = `Roles__DefaultUserMaxConfigs`, по умолчанию 3).
**У пользователя ровно одна роль**; его квота = `MaxConfigs` этой роли (`admin` → без лимита).
**Понижение роли (грандфазеринг)**: смену роли на роль с меньшей квотой разрешаем даже если текущих
конфигов больше новой квоты — существующие конфиги сохраняются, но **создание новых блокируется**,
пока число активных не станет меньше квоты. Форс-отзыв лишних не делаем.
### ActivationRequest — запрос активации
Пользователь просит активацию у админа; админ одобряет/отклоняет на сайте или в Telegram.
| Поле | Тип | Заметки |
| ------------ | ----------------------- | ---------------------------------------------------------- |
| `Id` | `Guid` | PK |
| `UserId` | `Guid` | FK → AppUser (заявитель) |
| `Comment` | `string?` | Комментарий заявителя, напр. «я Никита» — чтобы админ понял, кто это |
| `Status` | `ActivationStatus` | `Pending` / `Approved` / `Rejected` |
| `DecidedBy` | `Guid?` | Админ, принявший решение |
| `DecidedAt` | `DateTimeOffset?` | |
| `CreatedAt` | `DateTimeOffset` | |
Инварианты: одновременно не более одного `Pending`-запроса на пользователя; `Approved`
`AppUser.IsActivated = true`. Создание запроса и решение шлют realtime/Telegram-уведомления.
### TelegramLinkToken — токен привязки
Короткоживущий одноразовый токен для флоу привязки Telegram.
| Поле | Тип | Заметки |
| ------------ | ----------------- | ---------------------------------------- |
| `Id` | `Guid` | PK |
| `Token` | `string` | Высокоэнтропийный секрет (в deep-link) |
| `UserId` | `Guid` | FK → AppUser (кто привязывает) |
| `ExpiresAt` | `DateTimeOffset` | ≈2–5 минут |
| `ConsumedAt` | `DateTimeOffset?` | Одноразовый: гасится при использовании |
### TelegramLoginRequest — запрос passwordless-входа
Запрос входа на сайт без пароля, подтверждаемый в боте.
| Поле | Тип | Заметки |
| ------------ | ----------------------- | -------------------------------------------------------- |
| `Id` | `Guid` | PK; `nonce` в deep-link |
| `Status` | `TelegramLoginStatus` | `Pending` / `Approved` / `Rejected` / `Expired` / `Consumed` |
| `UserId` | `Guid?` | Проставляется после подтверждения (по `TelegramUserId`) |
| `Context` | `string?` | IP/устройство инициатора — показывается при подтверждении|
| `CreatedAt` | `DateTimeOffset` | |
| `ExpiresAt` | `DateTimeOffset` | ≈2–5 минут |
Переходы: `Pending → Approved/Rejected/Expired`; `Approved → Consumed` (после выпуска JWT сайту).
После `Consumed`/`Expired` — не переиспользуется.
## Value Objects
- **NodeCredentials** — `Username` + `ProtectedPassword` (шифротекст); равенство по значению; пароль не сериализуется наружу.
- **TrafficLimit** — байты; помощники `IsUnlimited`, `IsExceededBy(used)`, форматирование в ГБ.
- **ConnectionLink** — построенная ThreeXui.Net строка подключения + производные (подписка, QR-payload).
## Enums
```csharp
enum VpnProtocol { Vless, Vmess, Trojan, Shadowsocks }
enum NodeStatus { Unknown, Online, Offline }
enum ConfigStatus { Active, Disabled, Expired, LimitReached, Revoked }
enum TelegramLoginStatus { Pending, Approved, Rejected, Expired, Consumed }
enum ActivationStatus { Pending, Approved, Rejected }
enum AuditSource { Web, Telegram, System }
enum OsPlatform { iOS, Android, Windows, MacOS, Linux }
```
## Доменные события
| Событие | Когда | Реакция |
| ----------------------- | --------------------------------------- | --------------------------------------------------- |
| `VpnConfigCreated` | Успешно создан конфиг | Realtime-пуш владельцу; аудит |
| `VpnConfigRevoked` | Конфиг отозван | Realtime-пуш; аудит |
| `TrafficLimitReached` | `Used ≥ Limit` при синхронизации | (опц.) отключить клиента в 3x-ui; пуш; статус |
| `NodeWentOffline` | Health-probe вернул недоступность | Пуш группе `admins`; пометка статуса |
| `ActivationRequested` | Пользователь запросил активацию | Пуш `admins` + уведомление админам в Telegram |
| `UserActivated` | Админ одобрил активацию | Пуш владельцу + Telegram-DM (если привязан); аудит |
| `UserBlocked` / `UserUnblocked` | Админ (раз)блокировал пользователя | Отключить/включить конфиги в 3x-ui; пуш + Telegram-DM; аудит |
| `VpnConfigRotated` | Пользователь перевыпустил конфиг | Новый линк владельцу; аудит |
События публикуются из сущностей/хендлеров и обрабатываются `IDomainEventHandler<T>` в Application
(диспетчеризация — собственным диспетчером после `SaveChanges`); внешние эффекты (SignalR, 3x-ui) —
через порты, реализуемые в Infrastructure.
+113
View File
@@ -0,0 +1,113 @@
# Frontend
SPA на **React 19 + Vite + TypeScript**. Общается с бэком по REST (JWT Bearer) и получает
живые обновления по SignalR. Типы API генерируются из OpenAPI-схемы бэкенда.
> **Раздача из единого контейнера.** В проде собранный фронт (`dist/`) кладётся в `wwwroot`
> ASP.NET Core и раздаётся тем же приложением (SPA-fallback на `index.html`). Фронт и бек — один
> origin, база API — относительный `/api`, SignalR — `/hubs/panel`. В dev Vite-сервер проксирует
> `/api` и `/hubs` на бэкенд. Детали упаковки — [architecture.md](architecture.md#развёртывание-единый-контейнер-приложения).
## Стек
| Задача | Выбор |
| ----------------- | --------------------------------------- |
| Сборка/dev | Vite |
| Язык | TypeScript (strict) |
| Данные с сервера | TanStack Query |
| Роутинг | TanStack Router (типобезопасный) |
| UI-компоненты | shadcn/ui + Tailwind CSS v4 |
| Иконки | lucide-react |
| Клиентский стейт | Zustand (auth, тема) |
| Формы | react-hook-form + zod |
| Realtime | @microsoft/signalr |
| Графики | Recharts |
| QR-коды | qrcode.react |
| Типы API | openapi-typescript / orval (codegen) |
| i18n | react-i18next (RU + EN) |
| Пакетный менеджер | pnpm |
## Структура
```
frontend/
src/
app/ # провайдеры (Query, Router, Auth, Theme), корневой layout
routes/ # маршруты TanStack Router (login, dashboard, configs, admin/*)
features/
auth/ # формы, хуки useLogin/useRegister, стор авторизации
configs/ # список/создание/детали конфигов, QR, ссылка-подписка
nodes/ # (admin) управление нодами
admin/ # пользователи, статистика
shared/
api/ # http-клиент (fetch + JWT/refresh), сгенерированные типы, query-хуки
realtime/ # инициализация SignalR, подписки → инвалидация Query-кэша
ui/ # обёртки над shadcn/ui, общие компоненты
lib/ # утилиты, форматирование (байты, даты)
config/ # env, константы
styles/ # tailwind, темы
index.html
vite.config.ts
package.json
```
## Дизайн / UX
- **Тема оформления**: светлая и тёмная (переключатель в шапке; вариант «системная»). Реализация —
Tailwind `dark` (класс на `html`) + shadcn/ui; выбор сохраняется (localStorage).
- **Современный и чистый вид**: shadcn/ui + Tailwind, адаптивность, аккуратная типографика.
- **Дашборд пользователя**: карточки конфигов (протокол, локация, трафик прогресс-баром, срок, статус),
быстрые действия (копировать ссылку, показать QR, перевыпустить, отозвать) + карточка «Общая подписка»
(агрегированная ссылка/QR со всеми конфигами). Для неактивированного — экран «запросить активацию».
- **Создание конфига**: выбор локации/inbound (по `DisplayName`) + метка + лимит устройств →
мгновенная выдача ссылки + QR + краткие инструкции по подключению (iOS/Android/Windows).
- **Редактирование конфига**: изменить метку и лимит устройств.
- **Настройки аккаунта**: смена пароля, привязка/отвязка Telegram, **удаление аккаунта** (с подтверждением).
- **Админка**: таблицы (TanStack Table) с пагинацией/фильтрами для нод, пользователей, конфигов, ролей,
журнала аудита; очередь запросов активации; графики трафика (Recharts). Блокировка пользователя — с
подтверждением (гасит VPN).
- **Состояния**: скелетоны при загрузке, аккуратные пустые состояния и toasts на ошибки/успех.
## Работа с API
- HTTP-клиент оборачивает `fetch`: подставляет access-token, при 401 — прозрачно обновляет через
refresh-cookie и повторяет запрос; при неуспехе — разлогин.
- Все запросы/мутации — через TanStack Query (ключи по фичам, инвалидация после мутаций).
- Типы ответов/запросов — из codegen по OpenAPI (никакого ручного дублирования DTO).
## Авторизация на клиенте
- **Access-token** — в памяти (не в localStorage), кладётся в `Authorization: Bearer`.
- **Refresh-token** — httpOnly Secure cookie (JS не читает), ротация на сервере.
- Стор авторизации (Zustand) хранит профиль/роли/`isActivated`; guard-маршруты по роли (`admin` vs обычный)
и по активации (неактивированного ведём на экран «запросить активацию»).
- **Вход — по username** (email не используется). «Забыли пароль?» ведёт: при привязанном
Telegram — восстановление через бота; иначе — подсказка обратиться к админу.
- **Баннер привязки Telegram**: пока Telegram не привязан, показываем настойчивый, но не блокирующий
баннер/напоминание — это единственный self-service способ восстановить доступ. Настройки: смена пароля.
### Вход и привязка через Telegram
- **«Войти через Telegram»**: `POST /api/auth/telegram/login-request` → показать deep-link/QR на
бота, затем ждать подтверждения (поллинг `GET …/login-request/{id}` или событие SignalR). При
`Approved` — сохранить access, refresh уже в cookie, редирект в панель.
- **«Привязать Telegram»** (в настройках, для вошедшего): `POST /api/auth/telegram/link-token`
показать deep-link/QR; статус привязки обновить по факту (поллинг/SignalR). Отвязка — `unlink`.
- QR для deep-link — `qrcode.react`.
## Realtime
- Одно SignalR-подключение к `/hubs/panel` с JWT; реконнект с бэкоффом.
- Обработчики событий (`configTrafficUpdated`, `configStatusChanged`, `nodeStatusChanged`) точечно
обновляют/инвалидируют кэш TanStack Query — UI обновляется без перезагрузки.
## Скрипты (ожидаемые)
```bash
pnpm dev # dev-сервер Vite
pnpm build # прод-сборка
pnpm preview # предпросмотр сборки
pnpm lint # ESLint
pnpm typecheck # tsc --noEmit
pnpm gen:api # генерация типов из OpenAPI-схемы бэкенда
```
+89
View File
@@ -0,0 +1,89 @@
# Roadmap
Порядок реализации по этапам (milestones). Каждый этап — работоспособный инкремент.
## M0 — Каркас и инфраструктура
- Solution + 4 проекта (Domain/Application/Infrastructure/Api), ссылки по Clean Architecture.
- `Directory.Build.props`, `.editorconfig`, nullable + анализаторы, `dotnet format` в CI.
- EF Core + Npgsql, первая миграция.
- Scaffolding фронта: Vite + React + TS + Tailwind + shadcn/ui + TanStack Query/Router; dev-прокси `/api`,`/hubs` на бэк.
- **Единый контейнер**: multi-stage Dockerfile (node → dotnet publish → aspnet), Api раздаёт SPA из
`wwwroot` (fallback на `index.html`); docker-compose `app` + `db` (PostgreSQL).
- Health-check `/health`, Serilog, OpenAPI + Scalar.
- **Готово, когда**: единый образ поднимается в docker-compose рядом с postgres, отдаёт заглушку SPA и `/health`, есть базовая миграция.
## M1 — Аутентификация и сидинг
- ASP.NET Core Identity (`AppUser`/`AppRole` c `MaxConfigs`); `DbInitializer`: системные роли
`admin`/`user` и учётка админа + Telegram id админов из env ([`.env.example`](../.env.example)).
- **Вход по username** (email не используется); JWT access + refresh (httpOnly cookie, ротация, хранение
хэшей), CSRF на refresh, Identity lockout, rate-limit на `/auth/*`; смена пароля.
- Регистрация: новый пользователь → роль `user`, `IsActivated = false`.
- Фронт: страницы login/register (username), стор авторизации, refresh-flow, guard-маршруты.
- **Готово, когда**: регистрация/вход/refresh/logout по username работают, админ засидан, новый юзер неактивен.
## M2 — Роли и активация
- Домен: динамические роли (CRUD `admin`, квота `MaxConfigs`), `ActivationRequest`.
- Команды/запросы: CreateRole/UpdateRole/DeleteRole, ChangeUserRole (одна роль), RequestActivation (с комментарием),
ApproveActivation/RejectActivation.
- Эндпоинты активации (user + admin) и ролей; policy `RequireActivated`.
- Фронт: экран «запросить активацию» (с комментарием), админ-очередь запросов, управление ролями/назначением.
- **Готово, когда**: юзер запрашивает активацию с комментарием, админ на сайте активирует; роли с квотами работают.
## M3 — Ноды и публикация inbounds (по ролям)
- Домен `Node`/`Inbound` (+ `AllowedRoles`, `DisplayName`); порт `IXuiPanelGateway` + `XuiPanelGateway`
(per-node клиент, ThreeXui.Net); шифрование секретов нод (`ISecretProtector`).
- Команды/запросы: RegisterNode, SyncNode, Probe, ListNodes, ListInbounds, PublishInbound (с выбором ролей).
- Админка нод/инбаундов на фронте (публикация с `displayName` и `allowedRoleIds`).
- **Готово, когда**: админ подключает реальную 3x-ui и публикует inbound «Германия (Trojan)» для выбранных ролей.
## M4 — Конфиги пользователя (ядро продукта)
- Домен `VpnConfig` (создание, отзыв, ротация, статусы; инварианты: активирован + квота роли (грандфазеринг)
+ доступ роли к инбаунду; проверка квоты в транзакции; схема `ClientEmail`).
- CreateVpnConfig (с `label`/`deviceLimit``limitIp`), EditVpnConfig, RotateVpnConfig, RevokeVpnConfig,
GetMyConfigs, GetConfigLink, ListAvailableInbounds; connection string + QR.
- Подписка: агрегированная `/sub/{userToken}` (все конфиги) + по конфигу `/sub/{configToken}`;
заголовки `Subscription-Userinfo` / `profile-update-interval`.
- Самоудаление аккаунта (`DELETE /api/auth/me`): отзыв всех конфигов + удаление данных.
- Фронт: дашборд (метки, лимит устройств), создание/редактирование, инструкции подключения, копирование, QR, отзыв, перевыпуск, настройки аккаунта.
- **Готово, когда**: активированный юзер создаёт рабочий конфиг в доступном инбаунде в пределах квоты; работает агрегированная подписка.
## M5 — Синхронизация трафика и realtime
- `TrafficSyncService` (обход нод, обновление трафика/статусов, `TrafficSample`); реконсиляция дрейфа с 3x-ui.
- `NodeHealthCheckService`; `TrafficRetentionService` (TTL-чистка истории).
- SignalR `PanelHub` + `IRealtimeNotifier`; события трафика/статусов/нод/активации.
- Фронт: живые прогресс-бары трафика, статусы онлайн, реакция на превышение лимита/срока.
- **Готово, когда**: трафик и статусы обновляются в UI без перезагрузки.
## M6 — Админ-статистика, управление пользователями, аудит
- ListUsers, BlockUser (→ отключение конфигов в 3x-ui) / UnblockUser, ChangeUserRole, ResetUserPassword (без привязки TG), GetUserConfigs, force-revoke, GetStats.
- `AuditLog`: запись значимых действий (Web/Telegram/System) + эндпоинт `/api/admin/audit`.
- Фронт: таблицы пользователей/конфигов/ролей, журнал аудита, графики трафика (Recharts), сводки.
- **Готово, когда**: админ видит статистику и журнал, управляет пользователями/ролями/конфигами; блокировка гасит VPN.
## M7 — Telegram-бот
- Библиотека Telegram.Bot, `TelegramBotHostedService` (long polling) в процессе Api, `IOptions<TelegramOptions>`.
- Домен: поля Telegram у `AppUser`, `TelegramLinkToken`, `TelegramLoginRequest`.
- Флоу привязки (`LinkTelegramCommand`) + эндпоинт `link-token`/`unlink`.
- Passwordless-вход: `login-request` + подтверждение в боте (`ApproveTelegramLoginCommand`) → выпуск JWT; поллинг/SignalR-завершение на фронте.
- Восстановление пароля через бота (`/resetpassword` → одноразовая ссылка на смену пароля).
- Команды бота: `/start`, меню, «Мои конфиги» (`GetMyConfigsQuery`), «Открыть сайт», `/login`, `/unlink`, `/help`; QR в боте.
- **Админ в боте**: уведомления о запросах активации + inline «Активировать/Отклонить», `/requests` (по Telegram id из env).
- **DM-уведомления юзеру**: активация, отзыв конфига админом, блокировка (если Telegram привязан). Бот — read-only по конфигам.
- Фронт: кнопки «Войти через Telegram» и «Привязать Telegram» (deep-link/QR + ожидание подтверждения).
- **Готово, когда**: юзер привязывает Telegram, входит без пароля, видит конфиги; админ активирует запросы прямо в боте.
## M8 — Закалка (hardening)
- Полный набор тестов (Domain/Application/Integration с Testcontainers).
- Rate-limiting, аудит-лог действий, единообразные ProblemDetails, ретеншн `TrafficSample`.
- Прод-конфиг docker-compose (secrets, миграции отдельным шагом, опц. reverse-proxy для TLS).
- **Готово, когда**: зелёный CI, покрытие ключевых сценариев, готовность к деплою.
## Backlog (после MVP)
- Полное самообслуживание в боте (создание/ротация/отзыв конфигов) — в MVP бот read-only.
- Полная регистрация аккаунта через Telegram (в MVP — только привязка); Telegram Login Widget как альтернатива.
- Тарифы/биллинг/платежи, автопродление, промокоды.
- Реферальная программа; расширенные уведомления (через Telegram/веб — email в проекте не используется).
- Балансировка/выбор оптимальной ноды, автоскейл.
- OpenTelemetry-трейсинг, метрики, дашборды.
- Вынос фоновых задач в Hangfire/Quartz; TimescaleDB для истории трафика.
- Мультиязычность (RU/EN и далее).
+176
View File
@@ -0,0 +1,176 @@
# Tech Stack — решения и обоснование (ADR-lite)
Формат: **Решение** → короткое обоснование → альтернативы. Отклонения фиксировать здесь же.
## 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 стал платным). ~100 строк:
`ISender.Send()` резолвит `ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через
`IPipelineBehavior<,>` (валидация → авторизация → транзакция → логирование). Плюсы: нет лицензий и
внешних зависимостей, полный контроль. Доменные события — свой `IDomainEventHandler<T>` +
диспетчеризация после `SaveChanges`. Отклонены: MediatR (лицензия), FastEndpoints/Wolverine (лишняя связанность/переписывание модели).
### Валидация: FluentValidation
Декларативные валидаторы на команды/запросы, подключаются через `ValidationBehavior`.
### Маппинг: Mapster
Быстрый, без коммерческой лицензии (в отличие от AutoMapper, тоже ставшего платным), кодогенерация.
Для простых проекций — ручной `Select` в DTO без маппера.
### 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, обогащение контекста (`UserId`/`NodeId`/`ConfigId`/`CorrelationId`), секреты не
логируются. Синки MVP: Console (JSON в проде) + rolling file; Seq/OTel-экспорт — опционально позже.
Наблюдаемость сверх логов (OpenTelemetry-трейсинг, метрики) — вне MVP.
### API-документация: Swashbuckle (OpenAPI) + Scalar UI
Схема OpenAPI используется фронтом для кодогенерации типов. Scalar — современный UI вместо Swagger UI.
### Тесты: xUnit + FluentAssertions + NSubstitute + Testcontainers
Юнит-тесты домена/хендлеров (моками портов), интеграционные — с реальным PostgreSQL в Testcontainers.
## 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 codegen (openapi-typescript / orval)
Типы (и, опц., хуки) генерируются из OpenAPI-схемы бэкенда — single source of truth, никакого дрейфа контрактов.
### Графики: Recharts
Декларативные графики трафика/статистики. QR-коды конфигов — `qrcode.react`.
### i18n: react-i18next, RU + EN ✅ (зафиксировано)
**Решение принято**: локализация с первого дня, языки **RU + EN** (RU по умолчанию). Тексты — через
ключи (`react-i18next`), не хардкод строк в компонентах.
## Инфраструктура
### Упаковка: единый образ приложения + PostgreSQL
По требованию — **один контейнер на всё приложение** (REST + SignalR + Telegram-бот + статика SPA)
и отдельный контейнер БД.
- **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/куки/деплой и укладывается в требование
«фронт+бек в одном контейнере». Nginx/reverse-proxy — опция для прод (TLS-терминация) поверх, но не обязателен.
- **CI**: сборка/тесты бэка (`dotnet test`), линт/сборка фронта (`pnpm build`), сборка единого образа.
- **Пакетный менеджер фронта**: pnpm (быстрый, экономный по диску).
## Принятые решения (по открытым вопросам)
Все ключевые развилки закрыты:
| # | Вопрос | Решение |
| - | ------------------------------ | ------------------------------------------------------------------- |
| 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) |
### Продуктовые решения (поведение)
| Тема | Решение |
| ------------------------ | ------------------------------------------------------------------------------- |
| Вход | **По 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` — пользователь именует конфиг («Мой телефон») |
| Самоудаление аккаунта | Разрешено: отзыв всех конфигов + удаление данных, аудит анонимизируется |
| Версионирование API | Без версий в MVP (`/api` без `v1`) |
| Подписка (заголовки) | `Subscription-Userinfo` (used/total/expire) + `profile-update-interval` |
| Реконсиляция с 3x-ui | На синхронизации сверяем проекцию с панелью, помечаем дрейф, не «воскрешаем» молча |
Также заложены: CSRF-защита refresh-cookie + Identity lockout; проверка квоты в транзакции; схема
`ClientEmail = pnv_{userIdShort}_{rand}`; блокировка удаления ноды при наличии конфигов.
Осталось выбрать позже (не блокирует старт): значение TTL для истории трафика; конкретные синки
Serilog для прод (файл/Seq/OTel); точные TTL токенов Telegram. Email/SMTP в проекте **не используются**
(вход по username, восстановление — через Telegram/админа).
+150
View File
@@ -0,0 +1,150 @@
# Telegram Bot
Telegram-бот — **второй канал доставки** (presentation-адаптер) поверх той же Application-логики,
что и REST API. Он не содержит бизнес-правил: команды бота вызывают те же CQRS-команды/запросы
(`ICommand/IQuery`), что и веб. Бизнес-инварианты живут в домене, а не в обработчиках бота.
## Возможности
1. **Ссылка на сайт** — кнопка/команда, открывающая веб-панель (при желании — с одноразовым
deep-link авто-входом для уже привязанного пользователя).
2. **Мои конфиги** — список VPN-конфигов пользователя (протокол, локация, трафик, срок, статус),
ссылка-подписка и QR по каждому. Доступно только привязанному аккаунту.
3. **Авторизация через Telegram (passwordless)** — вход на сайт без пароля: подтверждение входа
в боте. Требует предварительной **привязки Telegram** к аккаунту.
4. **Админ: обработка запросов активации** — админ (по Telegram id из env) получает уведомление
о запросе активации с комментарием заявителя и жмёт «Активировать / Отклонить» прямо в боте.
5. **DM-уведомления пользователю** — если Telegram привязан, бот шлёт личные уведомления о ключевых
событиях: «аккаунт активирован», «конфиг отозван админом», «вы заблокированы».
6. **Восстановление пароля** — если пароль забыт, привязанный пользователь через бота получает
одноразовую ссылку на страницу задания нового пароля (или входит passwordless и меняет пароль в
настройках). Без привязки Telegram восстановление делает только админ.
> **Скоуп бота в MVP — просмотр (read-only) по конфигам.** Создание/ротация/отзыв конфигов — только
> на сайте. Полное самообслуживание в боте (создание/отзыв) — в backlog.
## Размещение в архитектуре
- Бот работает **в том же процессе**, что и API, как `BackgroundService`
(`TelegramBotHostedService`) — это укладывается в требование «фронт+бек в одном контейнере».
- Транспорт с Telegram: **long polling** для MVP (не требует публичного webhook-URL, проще в
одиночном контейнере). Webhook — опциональная альтернатива для прод-нагрузки (тогда — секретный
токен заголовка для верификации).
- Библиотека — **Telegram.Bot** (де-факто стандарт для C#).
- Код бота лежит в `PnvPanel.Api/Telegram/` (хендлеры апдейтов, построители клавиатур,
форматтеры сообщений). Обращения к домену — **только** через собственный `ISender`.
`Telegram.Bot` не проникает в Application/Domain.
```
Telegram ──updates──► TelegramBotHostedService (Api)
│ ISender.Send(command/query) // свой диспетчер
Application (те же хендлеры, что и REST)
```
## Модель данных (добавления)
- `AppUser.TelegramUserId : long?` — id пользователя Telegram (уникальный, nullable до привязки).
- `AppUser.TelegramUsername : string?`, `AppUser.TelegramLinkedAt : DateTimeOffset?`.
- `TelegramLinkToken` — короткоживущий одноразовый токен привязки (`token`, `userId`, `expiresAt`, `consumedAt`).
- `TelegramLoginRequest` — запрос passwordless-входа: `id/nonce`, `status`
(`Pending/Approved/Rejected/Expired/Consumed`), `userId?` (после подтверждения), `createdAt`, `expiresAt`.
Подробности полей — в [domain-model.md](domain-model.md).
## Флоу 1 — Привязка Telegram к аккаунту
Предусловие: пользователь уже вошёл на сайте (изначально аккаунт создаётся с username+пароль).
1. На сайте «Привязать Telegram» → `POST /api/auth/telegram/link-token``{ deepLink }`
вида `https://t.me/<bot>?start=link_<token>` (+ QR). Токен короткоживущий, одноразовый.
2. Пользователь открывает бота по ссылке → `/start link_<token>`.
3. Бот берёт `from.id` (Telegram user id), валидирует токен (`LinkTelegramCommand`), проставляет
`TelegramUserId`/`TelegramUsername`/`TelegramLinkedAt`, гасит токен.
4. Бот подтверждает: «Аккаунт привязан». Сайт узнаёт об успехе (поллинг статуса или SignalR).
Инварианты: один `TelegramUserId` ↔ один аккаунт; повторная привязка требует отвязки; токен
нельзя переиспользовать и он истекает.
## Флоу 2 — Passwordless-вход через бота
Предусловие: Telegram уже привязан к аккаунту.
1. На сайте «Войти через Telegram» → `POST /api/auth/telegram/login-request`
`{ requestId, deepLink, qr, expiresAt }`. Сайт начинает ждать результат (поллинг
`GET /api/auth/telegram/login-request/{requestId}` или событие SignalR).
2. Пользователь открывает `https://t.me/<bot>?start=login_<nonce>` → бот по `from.id` находит
привязанный аккаунт и показывает inline-кнопки **«Подтвердить вход / Отклонить»**
(с деталями: время, IP/устройство инициатора — для защиты от фишинга).
3. Подтверждение (`ApproveTelegramLoginCommand`) → запрос переходит в `Approved`, привязывается к `userId`.
4. Сайт (по поллингу/SignalR) получает результат: бэкенд выпускает **стандартные JWT**
access в теле ответа, refresh в httpOnly cookie. Запрос помечается `Consumed`.
Если Telegram **не привязан** — passwordless-вход невозможен (бот предлагает сперва привязать
аккаунт). Регистрация целиком через Telegram — вне MVP (см. backlog).
## Флоу 3 — Просмотр конфигов в боте
1. Привязанный пользователь: `/configs` или кнопка «Мои конфиги».
2. Бот вызывает `GetMyConfigsQuery` (тот же, что и веб) от имени `AppUser`, найденного по `TelegramUserId`.
3. Ответ — список с трафиком/сроком/статусом; по каждому конфигу — inline-кнопки «Ссылка», «QR».
QR отдаётся как изображение (генерация на сервере).
## Флоу 4 — Обработка активации админом в боте
1. Пользователь отправляет запрос активации (сайт: `POST /api/activation/request { comment }`);
доменное событие `ActivationRequested`.
2. Бот шлёт сообщение каждому админу (Telegram id из `AdminSeed__TelegramUserIds`) с username/комментарием
заявителя и inline-кнопками **«✅ Активировать / ❌ Отклонить»**.
3. Нажатие → `ApproveActivationCommand`/`RejectActivationCommand` (те же, что на сайте) → пользователь
активируется, ему уходит realtime-пуш `userActivated`, админам обновляется сообщение (решение зафиксировано).
4. Действие доступно только Telegram id из списка админов; проверка — на стороне бота перед вызовом команды.
## Команды и клавиатуры
| Команда / кнопка | Действие | Требует привязки |
| ---------------------- | -------------------------------------------------------------- | ---------------- |
| `/start` | Приветствие + меню (Открыть сайт / Мои конфиги / Войти) | нет |
| `/start link_<t>` | Привязка аккаунта по токену | нет |
| `/start login_<n>` | Подтверждение passwordless-входа | да |
| «Открыть сайт» | Ссылка на веб-панель (опц. одноразовый auto-login deep link) | нет / да |
| `/configs` | Список конфигов | да |
| `/login` | Инициировать/подтвердить вход | да |
| `/resetpassword` | Одноразовая ссылка на смену пароля (восстановление) | да |
| `/unlink` | Отвязать Telegram от аккаунта | да |
| `/help` | Справка | нет |
| «Активировать/Отклонить» | (admin) решение по запросу активации | админ по env |
| `/requests` | (admin) список ожидающих запросов активации | админ по env |
## Безопасность
- Токены привязки и nonce входа: высокоэнтропийные, **короткоживущие** (≈25 мин), **одноразовые**.
- Подтверждение входа показывает контекст (время/устройство) — защита от несанкционированных запросов.
- Верификация источника апдейтов: webhook — секретный заголовок; long polling — прямой канал к Bot API по TLS.
- Rate-limiting на создание login/link-запросов и на команды бота.
- Токен бота — секрет (env/secret-store), в логи не попадает; апдейты логируются без чувствительных данных.
- Passwordless-вход выпускает те же JWT/refresh, что и обычный — единые правила сессий и ротации.
- Альтернатива боту для веб-входа — официальный **Telegram Login Widget** (HMAC-подпись данных
ботом, верификация на бэке). Оставлено как опция; основной путь — подтверждение в боте.
## Конфигурация
```jsonc
"Telegram": {
"BotToken": "…", // секрет (env/secret-store)
"BotUsername": "PnvPanelBot",
"Mode": "LongPolling", // или "Webhook"
"WebhookUrl": null,
"WebhookSecret": null,
"PublicSiteUrl": "https://panel.example.com"
}
```
Telegram id администраторов задаются отдельно — `AdminSeed__TelegramUserIds` (см.
[`.env.example`](../.env.example)); именно они авторизуют админ-кнопки в боте и получают
уведомления о запросах активации.
Сообщения бота локализованы (**RU/EN**) по языку пользователя, синхронно с настройкой языка в вебе.
Строго типизированные `IOptions<TelegramOptions>` с валидацией на старте; при отсутствии
`BotToken` бот не стартует (панель работает без него).
+125
View File
@@ -0,0 +1,125 @@
# Product Vision & Scope
## Проблема
Раздача VPN-доступов через «голую» панель 3x-ui неудобна: администратор вручную заводит
клиентов, копирует ссылки, следит за трафиком и сроками. Конечные пользователи не имеют
самообслуживания — за каждым конфигом идут к админу.
## Решение
**PnvPanel** — тонкий, но красивый слой самообслуживания поверх одной или нескольких панелей
3x-ui:
- **Пользователь** регистрируется, сам создаёт себе VPN-конфиги, видит трафик/срок,
получает ссылку-подписку и QR-код, отзывает ненужные конфиги.
- **Администратор** подключает VPN-серверы (ноды 3x-ui), выбирает какие inbounds доступны для
самообслуживания, задаёт лимиты, управляет пользователями и видит статистику в реальном времени.
PnvPanel **не заменяет** Xray/3x-ui — он оркестрирует их через API (`ThreeXui.Net`) и хранит
свою проекцию данных (пользователи, привязки конфигов, история трафика) в PostgreSQL.
## Роли
| Роль | Возможности |
| ----------- | -------------------------------------------------------------------------------------------- |
| **Guest** | Регистрация, вход, публичный эндпоинт подписки (`/sub/{token}`). |
| **User** | После **активации** — CRUD своих конфигов (в рамках квоты роли и доступных инбаундов), просмотр трафика/срока, ссылка/QR, отзыв. |
| **Admin** | Всё выше без лимитов + ноды, публикация inbounds с выбором ролей, роли/квоты, активация пользователей, стата. |
| *(кастомные)* | Админ создаёт роли (напр. `vip`) со своей квотой конфигов и назначает их пользователям. |
### RBAC — динамические роли с квотой
- Роли реализованы через **ASP.NET Core Identity**, но `AppRole` расширен полем `MaxConfigs`
(квота на число конфигов). Авторизация — policy-based.
- Системные роли сидируются: `admin` (без лимита) и `user` (`MaxConfigs` из env, по умолчанию **3**).
- **Админ может создавать новые роли** с другой квотой и назначать их пользователям.
- **У пользователя ровно одна роль**; его квота = `MaxConfigs` этой роли.
### Активация пользователей
- После регистрации пользователь **не активирован** и не может создавать конфиги.
- Он отправляет **запрос на активацию** с комментарием (напр. «я Никита» — чтобы админ понял, кто это).
- Админ одобряет/отклоняет запрос **на сайте или в Telegram**. После одобрения — доступно создание конфигов.
### Аутентификация и восстановление доступа
- **Логин — по username** (email в системе не используется; SMTP не нужен).
- **Восстановление пароля**: только через привязанный Telegram (self-service). Если Telegram не
привязан — пароль сбрасывает админ.
- Пока Telegram не привязан, панель **настойчиво напоминает** это сделать (баннер/уведомления в UI) —
это единственный способ самому восстановить доступ.
### Сид администратора
- Учётка админа **сидируется при первом старте** из переменных окружения (username, пароль,
Telegram id админов). Пример — [`.env.example`](../.env.example). Telegram id админа задаётся через env
и используется для админ-действий и уведомлений в боте.
## Каналы доступа
- **Веб-панель** (React SPA) — основной интерфейс для User и Admin.
- **Telegram-бот** — вспомогательный канал для User: ссылка на сайт, просмотр своих конфигов и
**passwordless-вход** на сайт через привязанный Telegram (вместо пароля). Детали — [telegram-bot.md](telegram-bot.md).
## Ключевые пользовательские сценарии
### U0. Регистрация и активация
1. Пользователь регистрируется → получает роль `user`, статус **не активирован**.
2. Отправляет запрос на активацию с комментарием («я Никита»).
3. Админ видит запрос (на сайте и/или в Telegram) → «Активировать» / «Отклонить».
4. После одобрения пользователь может создавать конфиги (в пределах квоты роли).
### U1. Пользователь создаёт конфиг
1. Входит в панель (активирован) → «Создать конфиг».
2. Видит только инбаунды, **доступные его роли** (напр. «Германия (Trojan)»); выбирает нужный.
3. Проверка квоты: число активных конфигов < `MaxConfigs` его роли.
4. Бэкенд создаёт клиента в 3x-ui (`AddClient`), сохраняет привязку `VpnConfig` в БД.
5. Пользователь получает connection string, ссылку-подписку и QR-код.
### U2. Пользователь следит за трафиком
- Фоновая синхронизация тянет трафик из 3x-ui; изменения приходят в UI через SignalR (без перезагрузки).
- При достижении лимита/срока конфиг помечается и (опционально) отключается в 3x-ui.
### A1. Админ подключает ноду и публикует инбаунды
1. Вводит адрес панели 3x-ui, логин/пароль (шифруются при хранении).
2. PnvPanel проверяет доступность, синхронизирует список inbounds.
3. Админ публикует нужные inbounds (напр. «Германия (Trojan)») и **указывает роли**, которым
разрешено создавать конфиги в этом инбаунде (напр. `user`, `vip`).
### A3. Админ управляет ролями и активацией
1. Создаёт роль (напр. `vip`) с нужной квотой конфигов, назначает пользователям.
2. Обрабатывает запросы на активацию (на сайте или в Telegram): видит комментарий заявителя, решает.
### A2. Админ управляет пользователями
- Список пользователей, их конфигов и потребления; блокировка/разблокировка; принудительный отзыв конфигов.
### T1. Пользователь привязывает Telegram и входит без пароля
1. В веб-панели (войдя по username+паролю) нажимает «Привязать Telegram» → получает deep-link в бота.
2. Открывает бота → аккаунт привязывается к его Telegram.
3. В следующий раз на сайте выбирает «Войти через Telegram» → подтверждает вход в боте → входит без пароля.
4. В боте может смотреть свои конфиги и открывать сайт.
## Границы MVP
**В MVP входит:**
- Регистрация/вход (JWT + Identity); сид админа из env.
- Динамические роли с квотой конфигов (сид `admin`/`user`); создание ролей и назначение админом.
- Активация пользователей по запросу с комментарием (одобрение на сайте и в Telegram).
- Управление нодами; публикация inbounds с выбором доступных ролей.
- Создание/просмотр/отзыв конфигов пользователем (проверки активации, квоты, доступа роли к инбаунду); ссылка-подписка + QR.
- Синхронизация трафика (фоновая) + realtime-обновления по SignalR.
- Базовая статистика для админа.
- Telegram-бот: ссылка на сайт, просмотр конфигов, привязка Telegram и passwordless-вход.
- Единый Docker-образ (фронт+бек) + PostgreSQL в docker-compose.
**За рамками MVP (backlog):**
- Полная регистрация аккаунта через Telegram (в MVP — только привязка существующего).
- Тарифы/биллинг/платежи.
- Многоуровневые квоты, автопродление, промокоды.
- Балансировка нагрузки между нодами, автоскейл.
- Реферальная программа; расширенные уведомления (через Telegram/веб).
- Мультиязычность сверх RU/EN.
## Нефункциональные требования
- **Безопасность**: секреты нод шифруются at-rest; JWT с коротким TTL + refresh; rate-limiting на создание конфигов и auth.
- **Наблюдаемость**: структурные логи (Serilog), health-checks нод, метрики.
- **Отказоустойчивость к нодам**: недоступность одной ноды не роняет панель; операции идемпотентны где возможно.
- **Производительность**: списки с пагинацией; синхронизация трафика батчами.