From d8930409fec17044cb501cf0640b3c096dfc7d14 Mon Sep 17 00:00:00 2001 From: Leonid Pershin Date: Wed, 1 Jul 2026 18:37:54 +0300 Subject: [PATCH] Update .gitignore to include local environment files and expand README with project details, tech stack, documentation links, and project status. --- .env.example | 53 +++++++ .gitignore | 5 + CLAUDE.md | 172 ++++++++++++++++++++++ README.md | 45 ++++++ docs/README.md | 28 ++++ docs/api-design.md | 181 +++++++++++++++++++++++ docs/architecture.md | 243 ++++++++++++++++++++++++++++++ docs/backend-conventions.md | 115 +++++++++++++++ docs/domain-model.md | 285 ++++++++++++++++++++++++++++++++++++ docs/frontend.md | 113 ++++++++++++++ docs/roadmap.md | 89 +++++++++++ docs/tech-stack.md | 176 ++++++++++++++++++++++ docs/telegram-bot.md | 150 +++++++++++++++++++ docs/vision.md | 125 ++++++++++++++++ 14 files changed, 1780 insertions(+) create mode 100644 .env.example create mode 100644 CLAUDE.md create mode 100644 docs/README.md create mode 100644 docs/api-design.md create mode 100644 docs/architecture.md create mode 100644 docs/backend-conventions.md create mode 100644 docs/domain-model.md create mode 100644 docs/frontend.md create mode 100644 docs/roadmap.md create mode 100644 docs/tech-stack.md create mode 100644 docs/telegram-bot.md create mode 100644 docs/vision.md diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..a8e64b8 --- /dev/null +++ b/.env.example @@ -0,0 +1,53 @@ +# PnvPanel — пример переменных окружения. +# Скопируй в .env и заполни значения. Ключи вида Section__Key биндятся в IOptions ASP.NET Core. +# ВСЕ значения-секреты ниже обязательно заменить перед запуском (особенно *_PASSWORD, *SigningKey, *BotToken). + +# ── PostgreSQL (контейнер db) ───────────────────────────────────────────── +POSTGRES_DB=pnvpanel +POSTGRES_USER=pnvpanel +POSTGRES_PASSWORD=change-me-strong-db-password + +# Строка подключения приложения (host = имя сервиса БД в docker-compose) +ConnectionStrings__Default=Host=db;Port=5432;Database=pnvpanel;Username=pnvpanel;Password=change-me-strong-db-password + +# ── JWT ─────────────────────────────────────────────────────────────────── +Jwt__Issuer=PnvPanel +Jwt__Audience=PnvPanel +Jwt__SigningKey=change-me-min-32-chars-random-secret +Jwt__AccessTokenMinutes=15 +Jwt__RefreshTokenDays=30 + +# ── Data Protection (шифрование секретов нод 3x-ui at-rest) ──────────────── +# Путь к тому с key-ring (должен быть примонтирован и переживать перезапуск контейнера) +DataProtection__KeyRingPath=/app/keys + +# ── Сид администратора (создаётся при первом старте, если не существует) ─── +# Логин в систему — по username. Email в системе не используется. +AdminSeed__Username=admin +AdminSeed__Password=change-me-strong-admin-password +# Telegram id(ы) администраторов (через запятую). Дают права админа в боте +# и получают уведомления о запросах на активацию. Узнать id: @userinfobot. +AdminSeed__TelegramUserIds=123456789 + +# ── Роли по умолчанию ───────────────────────────────────────────────────── +# Квота конфигов для системной роли "user" (выдаётся при регистрации). +Roles__DefaultUserMaxConfigs=3 + +# ── Telegram-бот ────────────────────────────────────────────────────────── +# Если BotToken пуст — бот не стартует, панель работает без него. +Telegram__BotToken= +Telegram__BotUsername=PnvPanelBot +Telegram__Mode=LongPolling +# Для Mode=Webhook: +# Telegram__WebhookUrl=https://panel.example.com/tg/webhook +# Telegram__WebhookSecret=change-me-webhook-secret + +# ── Приложение ──────────────────────────────────────────────────────────── +# Публичный URL сайта (для deep-link'ов бота и ссылок). +App__PublicSiteUrl=https://panel.example.com +# CORS-источники (для dev; в проде фронт и бек — один origin). +App__CorsOrigins=http://localhost:5173 + +# ── ASP.NET Core ────────────────────────────────────────────────────────── +ASPNETCORE_ENVIRONMENT=Production +ASPNETCORE_HTTP_PORTS=8080 diff --git a/.gitignore b/.gitignore index 77575d5..66cf95a 100644 --- a/.gitignore +++ b/.gitignore @@ -414,3 +414,8 @@ FodyWeavers.xsd # JetBrains Rider *.sln.iml + +# Local environment files (secrets) — keep .env.example, ignore real .env +.env +.env.local +.env.*.local diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..d595c9e --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,172 @@ +# CLAUDE.md + +Инструкции для Claude Code при работе в этом репозитории. + +## Что это + +**PnvPanel** — self-service портал для VPN-конфигураций. Пользователи сами создают себе конфиги +(VLESS/VMess/Trojan/Shadowsocks), админ управляет серверами и пользователями. Есть **Telegram-бот** +(ссылка на сайт, просмотр конфигов, passwordless-вход через привязку Telegram). Бэкенд оркестрирует +панели **3x-ui** через библиотеку [`ThreeXui.Net`](https://github.com/mrleo1nid/ThreeXui.Net) и +хранит свою проекцию домена в PostgreSQL. Живые обновления — по SignalR. Приложение (фронт + бек + +бот) поставляется **единым Docker-образом**; PostgreSQL — отдельным контейнером в compose. + +> **Статус: проектирование.** Код ещё не написан. Актуальны только документация и этот файл. +> При старте реализации следуй [`docs/roadmap.md`](docs/roadmap.md) (этапы M0…M6). + +## Документация (single source of truth) + +Прежде чем менять архитектуру или добавлять фичу — свериться с [`docs/`](docs/README.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) +- [Frontend](docs/frontend.md) · [Telegram Bot](docs/telegram-bot.md) · [API Design](docs/api-design.md) · [Roadmap](docs/roadmap.md) + +**Держи доки в синхроне с кодом.** Меняешь контракт/архитектуру — обнови соответствующий док в том же изменении. + +## Стек + +- **Backend**: C# / .NET 10, ASP.NET Core Web API, Clean Architecture, CQRS (**собственный тонкий + диспетчер**, без MediatR), EF Core 10 + Npgsql (PostgreSQL), ASP.NET Core Identity + JWT, SignalR, + FluentValidation, Mapster, **Serilog** (логирование). +- **Frontend**: React 19 + Vite + TypeScript, TanStack Query/Router, shadcn/ui + Tailwind CSS v4, + Zustand, react-hook-form + zod, @microsoft/signalr, Recharts. Пакетный менеджер — pnpm. +- **Telegram**: Telegram.Bot, бот как `BackgroundService` **в процессе Api** (long polling). +- **Инфра**: единый Docker-образ (API + бот + статика SPA) + PostgreSQL в docker-compose. + +## Архитектура — жёсткие правила + +Слои и направление зависимостей: **Api → Infrastructure → Application → Domain** (внутрь). + +- **Domain** — без внешних зависимостей. Rich model: инварианты в сущностях (приватные сеттеры, + фабричные методы, поведенческие методы). Никаких EF/HTTP/Identity здесь. +- **Application** — CQRS-хендлеры, DTO, валидаторы, **порты** (интерфейсы). Зависит только от Domain. + Никаких `Npgsql`/`SignalR`/`ThreeXui.Net` — только их интерфейсы (`IAppDbContext`, `IXuiPanelGateway`, + `IRealtimeNotifier`, `ISecretProtector`, `ICurrentUser`, ...). +- **Infrastructure** — реализации портов: EF Core, Identity/JWT, `XuiPanelGateway`, SignalR-пуш, + фоновые сервисы, шифрование секретов. +- **Api** — Minimal API эндпоинты (по фичам), SignalR-хабы, middleware, DI composition root. + +Обязательно: +- CQRS: команды меняют состояние и идут в транзакции (UnitOfWorkBehavior); запросы только читают + (`AsNoTracking` + проекция в DTO). Диспетчер — **собственный** (`ISender`/`ICommandHandler`/ + `IQueryHandler`, регистрация хендлеров через DI), без внешних CQRS-библиотек. +- Управляемые ошибки — через `Result`, не исключениями. Исключения — только для исключительного. +- Валидация — FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода. +- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP. Никаких `.Result`/`.Wait()`. +- Nullable reference types включены; предупреждения анализаторов не игнорировать. + +## Интеграция с 3x-ui + +- Только через порт `IXuiPanelGateway`. `ThreeXui.Net` регистрируется на один `BaseAddress`, а нод + много → гейтвей держит **клиента per-node** (кэш по `NodeId`), создавая его из расшифрованных + `NodeCredentials`. Детали — в [architecture.md](docs/architecture.md#интеграция-с-3x-ui-threexuinet). +- Пароли нод **шифруются at-rest** (`ISecretProtector`), расшифровка только внутри Infrastructure, + никогда не в логах/ответах API. +- Недоступность ноды → `Result.Failure`/`NodeStatus.Offline`, не 500 наружу. +- Операции с 3x-ui идемпотентны; при частичном сбое (клиент создан в панели, но упала БД) — компенсация. + +## Роли, активация, сидинг + +- **Роли динамические**: `AppRole : IdentityRole` + поле `MaxConfigs` (квота на число конфигов). + Квота — **на роли, а не на `Plan`**. **У пользователя ровно одна роль**; квота = `MaxConfigs` его + роли (`admin` — без лимита). Системные роли (`admin`/`user`) не удалять/переименовывать. +- **Активация**: новый пользователь `IsActivated = false`, роль `user`. Конфиги может создавать + только активированный. `ActivationRequest` (с комментарием заявителя) одобряет админ на сайте + **или** в Telegram — одними и теми же командами (`ApproveActivationCommand`/`RejectActivationCommand`). +- **Инбаунды по ролям**: `Inbound.AllowedRoles` (M:N). При создании конфига доменный инвариант + проверяет: активирован + под квотой роли + роль входит в `AllowedRoles` инбаунда + нода включена. + Проверку квоты делать **в транзакции** (гонки параллельных созданий). +- **Понижение роли — грандфазеринг**: смена на меньшую квоту разрешена; лишние конфиги не отзываем, + но новые нельзя до входа в квоту. +- **Блокировка** (`AppUser.IsBlocked`): вход запрещён + все конфиги `Disabled` (отключить клиентов + в 3x-ui); разблокировка — обратно. Действие в `AuditLog`. +- **Аудит**: значимые действия (активация, блок, смена роли, отзыв, ноды/инбаунды) писать в `AuditLog` + (append-only, источник Web/Telegram/System). +- **Подписка**: агрегированная на юзера (`AppUser.SubscriptionToken`, все активные конфиги) + по конфигу. +- **Ротация конфига** (`Rotate()`): новый UUID/ссылка, квоту не тратит. **Бот в MVP — read-only** по конфигам. +- **Конфиг**: пользователь задаёт метку (`Label`) и лимит устройств (`DeviceLimit` → `limitIp` в 3x-ui, 0=без лимита), может редактировать. +- **Самоудаление аккаунта** (`DELETE /api/auth/me`): отзыв всех конфигов + удаление данных, аудит анонимизируется. +- **API без версионирования** в MVP (`/api` без `v1`). Подписка отдаёт `Subscription-Userinfo`. +- **Вход — по `UserName`** (email в системе не используется вовсе; SMTP не нужен). + Восстановление пароля: через привязанный Telegram (self-service), без привязки — сброс админом + (`ResetUserPasswordCommand`). Пока Telegram не привязан — UI настойчиво предлагает его привязать. +- **Сидинг из env**: идемпотентный `DbInitializer` на старте создаёт системные роли и учётку админа + (username/пароль/Telegram id) из переменных окружения. Единый источник примера — [`.env.example`](.env.example); + при добавлении новой настройки обновляй и его. Секреты (пароль админа, JWT-ключ, BotToken) — только через env/secret-store. +- Telegram id админов (`AdminSeed__TelegramUserIds`) авторизуют админ-действия в боте и получают + уведомления о запросах активации. + +## Telegram-бот + +- Бот — **presentation-адаптер**, не бизнес-слой. Хостится в процессе Api (`TelegramBotHostedService`, + long polling). Хендлеры апдейтов вызывают **те же** CQRS-команды/запросы через собственный `ISender` + (`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...). +- `Telegram.Bot` не проникает в Application/Domain — только в `Api/Telegram/`. +- Passwordless-вход выпускает **те же** JWT/refresh, что и обычный логин. Требует привязки Telegram + (в MVP — только привязка существующего аккаунта, регистрация из бота — backlog). +- Токены привязки/входа: короткоживущие, одноразовые, высокоэнтропийные. `Telegram:BotToken` — секрет, + не логировать. Панель должна работать и без бота (если токен не задан — бот просто не стартует). +- Детали флоу — [telegram-bot.md](docs/telegram-bot.md). + +## Единый контейнер + +- Один образ приложения: Api раздаёт REST (`/api`), SignalR (`/hubs`), хостит бота **и** статику SPA + из `wwwroot` (fallback на `index.html`). Фронт и бек — один origin, база API — относительный `/api`. +- Multi-stage Dockerfile: node (сборка фронта) → dotnet sdk (publish + копирование в `wwwroot`) → aspnet runtime. +- docker-compose: `app` (единый образ) + `db` (PostgreSQL). В dev — Vite-прокси `/api`,`/hubs` на бэк. +- Не вводи отдельный nginx-контейнер для статики без явной просьбы — это ломает требование единого контейнера. + +## Соглашения по коду + +Полный список — в [backend-conventions.md](docs/backend-conventions.md). Кратко: + +- Команды `Command`, запросы `Query`, + `Handler`/`Validator`. DTO — суффикс `Dto`. +- Application организована **по фичам** (feature folders) внутри слоёв. +- Один публичный тип на файл, имя файла = имя типа. Async-методы — суффикс `Async` + `CancellationToken`. +- Секреты не логировать; логи структурные (Serilog) с `UserId`/`NodeId`/`ConfigId`/`CorrelationId`. +- Ошибки API — единый `ProblemDetails`. + +## Команды (ожидаемые — появятся по мере создания проектов) + +Backend (из `backend/`): +```bash +dotnet build +dotnet test +dotnet run --project src/PnvPanel.Api +dotnet ef migrations add --project src/PnvPanel.Infrastructure --startup-project src/PnvPanel.Api +dotnet ef database update --project src/PnvPanel.Infrastructure --startup-project src/PnvPanel.Api +dotnet format +``` + +Frontend (из `frontend/`): +```bash +pnpm install +pnpm dev +pnpm build +pnpm lint && pnpm typecheck +pnpm gen:api # типы из OpenAPI-схемы бэкенда +``` + +Инфраструктура: +```bash +docker compose up -d # api + postgres (+ web) +``` + +> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент. +> Пути — с учётом Windows. + +## Принятые решения (зафиксированы) + +Ключевые развилки закрыты — см. [tech-stack.md](docs/tech-stack.md#принятые-решения-по-открытым-вопросам): +CQRS — **собственный диспетчер** (не MediatR); **одна роль** на пользователя; секреты нод — +**ASP.NET Data Protection**; тарифы `Plan` — **backlog** (в MVP без лимитов трафика/срока); +i18n — **RU+EN** (react-i18next); Telegram — **long polling**, только **привязка** (не signup); +история трафика — **простая таблица + TTL**; логирование — **Serilog**. + +## Рабочие принципы + +- Не начинай крупную реализацию без сверки с доками и, при неоднозначности, без вопроса пользователю. +- Соблюдай границы слоёв — это главный инвариант проекта. Нарушение = ошибка ревью. +- Обновляй документацию вместе с кодом. Не коммить и не пуши без явной просьбы. +- Отвечай пользователю на русском (язык общения в проекте — русский). diff --git a/README.md b/README.md index 6c2a899..61f6027 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,47 @@ # PnvPanel +**PnvPanel** — self-service портал для VPN-конфигураций. Пользователи самостоятельно создают +и управляют своими VPN-конфигами (VLESS / VMess / Trojan / Shadowsocks), а администраторы +управляют серверами, лимитами и пользователями. Есть **Telegram-бот** (ссылка на сайт, просмотр +конфигов, passwordless-вход через привязку Telegram). Под капотом — интеграция с панелями +[3x-ui](https://github.com/MHSanaei/3x-ui) через библиотеку +[ThreeXui.Net](https://github.com/mrleo1nid/ThreeXui.Net). Приложение (фронт + бек + бот) +поставляется **единым Docker-образом**; PostgreSQL — отдельным контейнером в compose. + +## Стек + +| Слой | Технологии | +| ----------- | -------------------------------------------------------------------------------- | +| Backend | C# / .NET 10, ASP.NET Core Web API, Clean Architecture, CQRS (свой диспетчер), EF Core | +| БД | PostgreSQL (Npgsql) | +| Auth | ASP.NET Core Identity + JWT (access + refresh) | +| Realtime | SignalR | +| Интеграция | ThreeXui.Net (3x-ui REST API) | +| Telegram | Telegram.Bot (in-process hosted service, long polling) | +| Frontend | React 19 + Vite + TypeScript, TanStack Query/Router, shadcn/ui + Tailwind | +| Упаковка | Единый Docker-образ (API + бот + статика SPA) + PostgreSQL в docker-compose | + +## Документация + +Проектная документация лежит в [`docs/`](docs/README.md): + +- [Product Vision & Scope](docs/vision.md) — что мы строим и для кого +- [Architecture](docs/architecture.md) — Clean Architecture, CQRS, интеграция, realtime, безопасность +- [Domain Model](docs/domain-model.md) — сущности, связи, инварианты +- [Tech Stack (ADR)](docs/tech-stack.md) — решения по стеку и их обоснование +- [Backend Conventions](docs/backend-conventions.md) — структура проекта, паттерны, стиль кода +- [Frontend](docs/frontend.md) — стек фронтенда и структура +- [Telegram Bot](docs/telegram-bot.md) — бот, привязка Telegram и passwordless-вход +- [API Design](docs/api-design.md) — REST-эндпоинты и SignalR-контракты +- [Roadmap](docs/roadmap.md) — этапы разработки + +Пример переменных окружения (сид админа, БД, JWT, Telegram) — [`.env.example`](.env.example). +Инструкции для AI-ассистента (Claude Code) — в [`CLAUDE.md`](CLAUDE.md). + +## Статус + +🚧 Проектирование. Кодовая база ещё не создана — на этом этапе зафиксированы архитектура и план. + +## Лицензия + +[MIT](LICENSE) diff --git a/docs/README.md b/docs/README.md new file mode 100644 index 0000000..fc9c458 --- /dev/null +++ b/docs/README.md @@ -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. diff --git a/docs/api-design.md b/docs/api-design.md new file mode 100644 index 0000000..158859a --- /dev/null +++ b/docs/api-design.md @@ -0,0 +1,181 @@ +# API Design + +REST поверх HTTP/JSON, авторизация — `Authorization: Bearer ` (кроме публичных). +Ошибки — `application/problem+json` (`ProblemDetails`). Пагинация — `?page=&pageSize=`, +ответ `PagedList` (`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 (при необходимости)| diff --git a/docs/architecture.md b/docs/architecture.md new file mode 100644 index 0000000..d8b1f96 --- /dev/null +++ b/docs/architecture.md @@ -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 │ +└───────────────┬───────────────────────────────┬──────────────────────────┘ + │ 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**: явная модель успеха/ошибки вместо исключений для управляемых сценариев. + +### 3. `PnvPanel.Infrastructure` +Технические детали и реализации портов. + +- **Persistence**: `AppDbContext : IdentityDbContext`, реализует `IAppDbContext`; + `IEntityTypeConfiguration` для маппингов; миграции 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`; выполняются в транзакции (UnitOfWorkBehavior). +- **Запросы** только читают; могут ходить в БД проекциями (`Select` в DTO) без загрузки сущностей целиком. +- Диспетчер — **собственный тонкий `ISender`**: резолвит хендлер команды/запроса из DI и прогоняет + через pipeline behaviors. Без внешних CQRS-библиотек (MediatR/и т.п.). Абстракции — `ICommand`, + `IQuery`, `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` полем `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` → маппинг в 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) │ +└─────────────────────────────────────────────────────┘ +``` diff --git a/docs/backend-conventions.md b/docs/backend-conventions.md new file mode 100644 index 0000000..8f132a3 --- /dev/null +++ b/docs/backend-conventions.md @@ -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, IQuery, ICommandHandler<,>, IQueryHandler<,>, IPipelineBehavior<,> + Models/ # Result, Error, PagedList + 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 + 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`. +- Команды — `Command` (`CreateVpnConfigCommand`), запросы — `Query`. +- Хендлеры — `Handler`; валидаторы — `Validator`. +- DTO — суффикс `Dto` (`VpnConfigDto`); ответы эндпоинтов — `Response`, тела запросов — `Request`. +- Async-методы — суффикс `Async`, всегда принимают `CancellationToken`. +- Один публичный тип на файл; имя файла = имя типа. + +## Паттерны + +- **Rich domain model**: инварианты в сущностях (приватные сеттеры, фабричные методы `Node.Create(...)`, + поведенческие методы `config.Revoke()`), а не анемичные DTO-сущности. +- **CQRS через собственный диспетчер**: хендлеры реализуют `ICommandHandler` / + `IQueryHandler<,>`; `ISender` резолвит их из DI и прогоняет через `IPipelineBehavior<,>` + (валидация, транзакция, логирование). Без внешних CQRS-библиотек. +- **Порты в Application, адаптеры в Infrastructure**: никакого `Npgsql`/`SignalR`/`ThreeXui.Net` в Application/Domain. +- **`IAppDbContext`** экспонирует `DbSet<>` и `SaveChangesAsync`; хендлеры пишут LINQ напрямую + (репозитории — только для сложной агрегатной логики). +- **Result-модель**: команды/запросы возвращают `Result`; эндпоинт маппит в 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` для секций конфига; валидация опций на старте. + +## Стиль и качество кода + +- `.editorconfig` + анализаторы (`Microsoft.CodeAnalysis.NetAnalyzers`), nullable reference types **включены**. +- `dotnet format` в CI; предупреждения как ошибки для наших проектов. +- Комментарии — по необходимости (почему, а не что); публичные контракты портов документируем XML-doc. diff --git a/docs/domain-model.md b/docs/domain-model.md new file mode 100644 index 0000000..7179cd0 --- /dev/null +++ b/docs/domain-model.md @@ -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`. Роли **создаёт админ** и назначает пользователям; роль несёт квоту +на число конфигов. + +| Поле | Тип | Заметки | +| ------------ | -------- | --------------------------------------------------------------- | +| `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` в Application +(диспетчеризация — собственным диспетчером после `SaveChanges`); внешние эффекты (SignalR, 3x-ui) — +через порты, реализуемые в Infrastructure. diff --git a/docs/frontend.md b/docs/frontend.md new file mode 100644 index 0000000..bdeb145 --- /dev/null +++ b/docs/frontend.md @@ -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-схемы бэкенда +``` diff --git a/docs/roadmap.md b/docs/roadmap.md new file mode 100644 index 0000000..4aa40bd --- /dev/null +++ b/docs/roadmap.md @@ -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`. +- Домен: поля 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 и далее). diff --git a/docs/tech-stack.md b/docs/tech-stack.md new file mode 100644 index 0000000..773721e --- /dev/null +++ b/docs/tech-stack.md @@ -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` + +диспетчеризация после `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` с валидацией на старте. + +### 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` (или 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/админа). diff --git a/docs/telegram-bot.md b/docs/telegram-bot.md new file mode 100644 index 0000000..95e9da3 --- /dev/null +++ b/docs/telegram-bot.md @@ -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/?start=link_` (+ QR). Токен короткоживущий, одноразовый. +2. Пользователь открывает бота по ссылке → `/start link_`. +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/?start=login_` → бот по `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_` | Привязка аккаунта по токену | нет | +| `/start login_` | Подтверждение passwordless-входа | да | +| «Открыть сайт» | Ссылка на веб-панель (опц. одноразовый auto-login deep link) | нет / да | +| `/configs` | Список конфигов | да | +| `/login` | Инициировать/подтвердить вход | да | +| `/resetpassword` | Одноразовая ссылка на смену пароля (восстановление) | да | +| `/unlink` | Отвязать Telegram от аккаунта | да | +| `/help` | Справка | нет | +| «Активировать/Отклонить» | (admin) решение по запросу активации | админ по env | +| `/requests` | (admin) список ожидающих запросов активации | админ по env | + +## Безопасность + +- Токены привязки и nonce входа: высокоэнтропийные, **короткоживущие** (≈2–5 мин), **одноразовые**. +- Подтверждение входа показывает контекст (время/устройство) — защита от несанкционированных запросов. +- Верификация источника апдейтов: 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` с валидацией на старте; при отсутствии +`BotToken` бот не стартует (панель работает без него). diff --git a/docs/vision.md b/docs/vision.md new file mode 100644 index 0000000..68bb27d --- /dev/null +++ b/docs/vision.md @@ -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 нод, метрики. +- **Отказоустойчивость к нодам**: недоступность одной ноды не роняет панель; операции идемпотентны где возможно. +- **Производительность**: списки с пагинацией; синхронизация трафика батчами.