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