Files
PnvPanel/docs/tech-stack.md
T

183 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# Tech Stack — решения и обоснование (ADR-lite)
Формат: **Решение** → короткое обоснование → альтернативы. Отклонения фиксировать здесь же.
## Backend
### Платформа: .NET 10 + ASP.NET Core Web API
Долгосрочная (LTS-класса) современная платформа, нативная поддержка Minimal API, rate limiting,
health checks, DI. `ThreeXui.Net` таргетит `net10.0` — совпадение целевого фреймворка.
### Архитектура: Clean Architecture (4 проекта)
`Domain / Application / Infrastructure / Api`. Тестируемость, изоляция домена, заменяемость инфраструктуры.
Альтернативы: Vertical Slice (проще для мелких API, но хуже изолирует домен для растущего продукта) —
можно комбинировать: слои + организация Application «по фичам».
### CQRS: собственный тонкий диспетчер ✅ (зафиксировано)
**Решение принято**: свой `ISender` вместо MediatR (тот с v12 стал платным). ~100 строк:
`ISender.Send()` резолвит `ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через
`IPipelineBehavior<,>` (валидация → авторизация → транзакция → логирование). Плюсы: нет лицензий и
внешних зависимостей, полный контроль. Доменные события — свой `IDomainEventHandler<T>` +
диспетчеризация после `SaveChanges`. Отклонены: MediatR (лицензия), FastEndpoints/Wolverine (лишняя связанность/переписывание модели).
### Валидация: FluentValidation
Декларативные валидаторы на команды/запросы, подключаются через `ValidationBehavior`.
### Маппинг: Mapster
Быстрый, без коммерческой лицензии (в отличие от AutoMapper, тоже ставшего платным), кодогенерация.
Для простых проекций — ручной `Select` в DTO без маппера.
### ORM: EF Core 10 + Npgsql
Миграции, LINQ, `IEntityTypeConfiguration`. Провайдер PostgreSQL — Npgsql.
Запросы-чтения — проекции в DTO (`AsNoTracking` + `Select`).
### БД: PostgreSQL
Надёжная, богатая по типам (jsonb, массивы), бесплатная. Для истории трафика в будущем —
TimescaleDB-расширение.
### Auth: ASP.NET Core Identity + JWT
Identity для пользователей/ролей/хэширования; JWT access (короткий TTL) + refresh (httpOnly cookie, ротация).
Альтернатива — внешний OIDC (Keycloak/Auth0); отклонено на этом этапе в пользу полного контроля.
### RBAC: динамические роли с квотой (`AppRole.MaxConfigs`)
Роли — стандартный Identity, но `AppRole` расширен `MaxConfigs`. Админ создаёт/назначает роли;
доступ к инбаундам — по ролям (`Inbound.AllowedRoles`). Квота на число конфигов — на роли, а не на `Plan`.
### Активация пользователей
`AppUser.IsActivated` + `ActivationRequest` (с комментарием). Неактивированный не создаёт конфиги.
Решение принимает админ на сайте или в Telegram — одними и теми же CQRS-командами.
### Сидинг из env
Идемпотентный `DbInitializer` на старте: системные роли (`admin`/`user`), учётка админа и Telegram id
админов — из переменных окружения. Пример — [`.env.example`](../.env.example). Строго типизированные
`IOptions<T>` с валидацией на старте.
### Realtime: SignalR
Нативно для ASP.NET Core, авто-транспорт (WebSocket→SSE→long-poll), группы/пользователи, JWT-авторизация хабов.
### Telegram-бот: Telegram.Bot (in-process hosted service)
Де-факто стандартная C#-библиотека. Бот хостится в процессе Api как `BackgroundService` (условие
единого контейнера) и вызывает те же CQRS-хендлеры, что и REST. Транспорт — **long polling** для
MVP (не нужен публичный webhook, проще в одиночном контейнере); webhook — опция для прод (с секретным
заголовком). Passwordless-вход выпускает те же JWT/refresh, что и веб. Детали — [telegram-bot.md](telegram-bot.md).
### Фоновые задачи: BackgroundService + PeriodicTimer (MVP)
Без внешних зависимостей для MVP. При росте (ретраи, расписания, дашборд) — **Hangfire** или **Quartz.NET**.
### Result-модель: собственный `Result<T>` (или ErrorOr)
Явные ошибки вместо исключений для управляемых сценариев; исключения — только для действительно исключительного.
### Логирование: Serilog ✅ (зафиксировано)
**Решение принято**: структурное логирование — **Serilog** (`Serilog.AspNetCore`). Настройка через
`appsettings`/env, обогащение контекста (`UserId`/`NodeId`/`ConfigId`/`CorrelationId`), секреты не
логируются. Синки MVP: Console (JSON в проде) + rolling file; Seq/OTel-экспорт — опционально позже.
Наблюдаемость сверх логов (OpenTelemetry-трейсинг, метрики) — вне MVP.
### API-документация: Swashbuckle (OpenAPI) + Scalar UI
Схема OpenAPI используется фронтом для кодогенерации типов. Scalar — современный UI вместо Swagger UI.
### Тесты: xUnit + FluentAssertions + NSubstitute + Testcontainers
Юнит-тесты домена/хендлеров (моками портов), интеграционные — с реальным PostgreSQL в Testcontainers.
## Frontend
### React 19 + Vite + TypeScript
Максимальная экосистема, быстрый dev-сервер и сборка Vite, строгая типизация. SPA (не SSR) —
для внутренней панели SSR избыточен и усложняет деплой рядом с C# API.
### Данные с сервера: TanStack Query
Кэш, инвалидация, фоновые рефетчи, статусы загрузки/ошибок. Идеально для CRUD-панели.
### Роутинг: TanStack Router
Типобезопасный роутинг, интеграция с TanStack Query. Альтернатива — React Router 7.
### UI: shadcn/ui + Tailwind CSS v4
Копируемые в проект, полностью кастомизируемые компоненты (Radix под капотом), современный вид,
тёмная тема из коробки. Иконки — `lucide-react`.
### Клиентский стейт: Zustand
Лёгкий стор для глобального (авторизация, тема). Серверный стейт — только в TanStack Query.
### Формы: react-hook-form + zod
Производительные формы + схемная валидация; те же zod-схемы для типобезопасности API-ответов.
### Realtime: @microsoft/signalr
Официальный клиент SignalR; подписки на события хаба обновляют кэш TanStack Query.
### Типы API: OpenAPI codegen (openapi-typescript / orval)
Типы (и, опц., хуки) генерируются из OpenAPI-схемы бэкенда — single source of truth, никакого дрейфа контрактов.
### Графики: Recharts
Декларативные графики трафика/статистики. QR-коды конфигов — `qrcode.react`.
### i18n: react-i18next, RU + EN ✅ (зафиксировано)
**Решение принято**: локализация с первого дня, языки **RU + EN** (RU по умолчанию). Тексты — через
ключи (`react-i18next`), не хардкод строк в компонентах.
## Инфраструктура
### Упаковка: единый образ приложения + PostgreSQL
По требованию — **один контейнер на всё приложение** (REST + SignalR + Telegram-бот + статика SPA)
и отдельный контейнер БД.
- **Multi-stage Dockerfile**: (1) `node` собирает фронт → `dist/`; (2) `dotnet sdk` публикует Api и
копирует статику в `wwwroot`; (3) `aspnet` runtime запускает Api. Api раздаёт SPA (`UseStaticFiles`
+ fallback на `index.html`), фронт и бек — один origin.
- **docker-compose**: `app` (единый образ) + `db` (PostgreSQL) с томом.
- Почему не отдельный nginx: единый origin упрощает CORS/куки/деплой и укладывается в требование
«фронт+бек в одном контейнере».
- **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 (быстрый, экономный по диску).
## Принятые решения (по открытым вопросам)
Все ключевые развилки закрыты:
| # | Вопрос | Решение |
| - | ------------------------------ | ------------------------------------------------------------------- |
| 1 | CQRS-медиатор | **Собственный тонкий диспетчер** (не MediatR) |
| 2 | Ролей у пользователя | **Ровно одна роль** (квота = `MaxConfigs` роли) |
| 3 | Секреты нод | **ASP.NET Core Data Protection** (шифрование at-rest, key-ring на томе) |
| 4 | Тарифы `Plan` в MVP | **Backlog** — в MVP конфиги без лимитов трафика/срока |
| 5 | i18n | **RU + EN** с первого дня (react-i18next) |
| 6 | Telegram-транспорт | **Long polling** |
| 7 | Регистрация через Telegram | **Только привязка** существующего аккаунта (signup из бота — backlog) |
| 8 | История трафика `TrafficSample`| **Простая таблица PostgreSQL + TTL** (фоновая чистка старше N дней) |
| 9 | Логирование | **Serilog** (Console + rolling file) |
### Продуктовые решения (поведение)
| Тема | Решение |
| ------------------------ | ------------------------------------------------------------------------------- |
| Вход | **По username** (email в системе не используется; SMTP не нужен) |
| Восстановление пароля | Через привязанный Telegram (self-service); без привязки — сброс админом |
| Побуждение привязать TG | Настойчивый баннер/уведомления в UI, пока Telegram не привязан |
| Регистрация | Открытая + гейт активации админом |
| Конфиги в одном инбаунде | Разрешено несколько (ограничение — только общая квота роли) |
| Данные ноды пользователю | Показываем только `DisplayName` + протокол; адрес/хост/порт скрыты |
| Блокировка пользователя | Отключать все его конфиги в 3x-ui (`Disabled`); разблокировка — включить обратно |
| Понижение роли | **Грандфазеринг**: существующие конфиги живут, новые нельзя до входа в квоту |
| Скоуп Telegram-бота (MVP)| **Read-only** по конфигам (создание/отзыв — на сайте); DM-уведомления юзеру |
| Подписка | Агрегированная на юзера (`AppUser.SubscriptionToken`) + по конфигу |
| Аудит | `AuditLog` (append-only): активация, блокировка, смена роли, отзыв, ноды/инбаунды |
| Ротация конфига | `Rotate()` — перевыпуск UUID/ссылки, квоту не тратит (на случай утечки) |
| Лимит устройств | Per-config, задаёт юзер (`DeviceLimit``limitIp` в 3x-ui; 0 = без лимита) |
| Метка конфига | `Label` — пользователь именует конфиг («Мой телефон») |
| Самоудаление аккаунта | Разрешено: отзыв всех конфигов + удаление данных, аудит анонимизируется |
| Версионирование API | Без версий в MVP (`/api` без `v1`) |
| Подписка (заголовки) | `Subscription-Userinfo` (used/total/expire) + `profile-update-interval` |
| Тема сайта | Светлая + тёмная (+ системная); Tailwind `dark`, выбор в localStorage |
| Инструкции/приложения | Отдельная страница инструкций + каталог `ClientApp` (админ CRUD, юзер — по ОС); стартовый сид из `seed/client-apps.json` |
| Реконсиляция с 3x-ui | На синхронизации сверяем проекцию с панелью, помечаем дрейф, не «воскрешаем» молча |
Также заложены: CSRF-защита refresh-cookie + Identity lockout; проверка квоты в транзакции; схема
`ClientEmail = pnv_{userIdShort}_{rand}`; блокировка удаления ноды при наличии конфигов.
Осталось выбрать позже (не блокирует старт): значение TTL для истории трафика; конкретные синки
Serilog для прод (файл/Seq/OTel); точные TTL токенов Telegram. Email/SMTP в проекте **не используются**
(вход по username, восстановление — через Telegram/админа).