Files
PnvPanel/docs/tech-stack.md
T

16 KiB
Raw Blame History

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. Строго типизированные 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.

Фоновые задачи: 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, задаёт юзер (DeviceLimitlimitIp в 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/админа).