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