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