16 KiB
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)aspnetruntime запускает Api. Api раздаёт SPA (UseStaticFiles- fallback на
index.html), фронт и бек — один origin.
- fallback на
- docker-compose:
app(единый образ) +db(PostgreSQL) с томом. - Почему не отдельный nginx: единый origin упрощает CORS/куки/деплой и укладывается в требование «фронт+бек в одном контейнере». Nginx/reverse-proxy — опция для прод (TLS-терминация) поверх, но не обязателен.
- CI: сборка/тесты бэка (
dotnet test), линт/сборка фронта (pnpm build), сборка единого образа. - Пакетный менеджер фронта: pnpm (быстрый, экономный по диску).
Принятые решения (по открытым вопросам)
Все ключевые развилки закрыты:
| # | Вопрос | Решение |
|---|---|---|
| 1 | CQRS-медиатор | Собственный тонкий диспетчер (не MediatR) |
| 2 | Ролей у пользователя | Ровно одна роль (квота = MaxConfigs роли) |
| 3 | Секреты нод | ASP.NET Core Data Protection (шифрование at-rest, key-ring на томе) |
| 4 | Тарифы Plan в MVP |
Backlog — в MVP конфиги без лимитов трафика/срока |
| 5 | i18n | RU + EN с первого дня (react-i18next) |
| 6 | Telegram-транспорт | Long polling |
| 7 | Регистрация через Telegram | Только привязка существующего аккаунта (signup из бота — backlog) |
| 8 | История трафика TrafficSample |
Простая таблица PostgreSQL + TTL (фоновая чистка старше N дней) |
| 9 | Логирование | Serilog (Console + rolling file) |
Продуктовые решения (поведение)
| Тема | Решение |
|---|---|
| Вход | По username (email в системе не используется; SMTP не нужен) |
| Восстановление пароля | Через привязанный Telegram (self-service); без привязки — сброс админом |
| Побуждение привязать TG | Настойчивый баннер/уведомления в UI, пока Telegram не привязан |
| Регистрация | Открытая + гейт активации админом |
| Конфиги в одном инбаунде | Разрешено несколько (ограничение — только общая квота роли) |
| Данные ноды пользователю | Показываем только DisplayName + протокол; адрес/хост/порт скрыты |
| Блокировка пользователя | Отключать все его конфиги в 3x-ui (Disabled); разблокировка — включить обратно |
| Понижение роли | Грандфазеринг: существующие конфиги живут, новые нельзя до входа в квоту |
| Скоуп Telegram-бота (MVP) | Read-only по конфигам (создание/отзыв — на сайте); DM-уведомления юзеру |
| Подписка | Агрегированная на юзера (AppUser.SubscriptionToken) + по конфигу |
| Аудит | AuditLog (append-only): активация, блокировка, смена роли, отзыв, ноды/инбаунды |
| Ротация конфига | Rotate() — перевыпуск UUID/ссылки, квоту не тратит (на случай утечки) |
| Лимит устройств | Per-config, задаёт юзер (DeviceLimit → limitIp в 3x-ui; 0 = без лимита) |
| Метка конфига | Label — пользователь именует конфиг («Мой телефон») |
| Самоудаление аккаунта | Разрешено: отзыв всех конфигов + удаление данных, аудит анонимизируется |
| Версионирование API | Без версий в MVP (/api без v1) |
| Подписка (заголовки) | Subscription-Userinfo (used/total/expire) + profile-update-interval |
| Реконсиляция с 3x-ui | На синхронизации сверяем проекцию с панелью, помечаем дрейф, не «воскрешаем» молча |
Также заложены: CSRF-защита refresh-cookie + Identity lockout; проверка квоты в транзакции; схема
ClientEmail = pnv_{userIdShort}_{rand}; блокировка удаления ноды при наличии конфигов.
Осталось выбрать позже (не блокирует старт): значение TTL для истории трафика; конкретные синки Serilog для прод (файл/Seq/OTel); точные TTL токенов Telegram. Email/SMTP в проекте не используются (вход по username, восстановление — через Telegram/админа).