# Tech Stack ## Backend - **Платформа**: .NET 10, ASP.NET Core Web API (Minimal API). - **Архитектура**: Clean Architecture, 4 проекта — `Domain / Application / Infrastructure / Api`. - **CQRS**: собственный тонкий диспетчер (`ISender`), без MediatR. `ISender.Send()` резолвит `ICommandHandler<,>`/`IQueryHandler<,>` из DI и прогоняет через `IPipelineBehavior<,>`: `ValidationBehavior` (FluentValidation), `LoggingBehavior`, `UnitOfWorkBehavior` (транзакция + `SaveChangesAsync` на команду). Авторизация проверяется на уровне эндпоинта (`RequireAuthorization(...)`), более тонкие проверки (владение, активация) — в хендлере. - **Валидация**: FluentValidation, подключается через `ValidationBehavior` (не для каждой команды — только там, где есть что проверить помимо типов). - **Маппинг**: вручную, статический метод `XxxDto.FromDomain(entity)` на самом DTO. - **ORM**: EF Core 10 + Npgsql. Миграции, `IEntityTypeConfiguration`. Запросы-чтения — проекции в DTO (`AsNoTracking` + `Select`). - **БД**: PostgreSQL. - **Auth**: ASP.NET Core Identity + JWT (access, короткий TTL) + refresh (httpOnly cookie, ротация). - **RBAC**: динамические роли с квотой (`AppRole.MaxConfigs`). Доступ к инбаундам — по ролям (`Inbound.AllowedRoles`). Квота на число конфигов — на роли, а не на тариф. - **Активация пользователей**: `AppUser.IsActivated` + `ActivationRequest` (с комментарием). Неактивированный не создаёт конфиги; решение принимает админ на сайте или в Telegram — одними и теми же CQRS-командами. - **Сидинг из env**: идемпотентный `DbInitializer` на старте — системные роли (`admin`/`user`), учётка админа и Telegram id админов. Пример — [`.env.example`](../.env.example). - **Realtime**: SignalR — авто-транспорт (WebSocket→SSE→long-poll), группы/пользователи, JWT-авторизация хабов. - **Telegram-бот**: Telegram.Bot, хостится в процессе Api как `BackgroundService` (long polling) и вызывает те же CQRS-хендлеры, что и REST. Passwordless-вход выпускает те же JWT/refresh, что и веб. Детали — [telegram-bot.md](telegram-bot.md). - **Фоновые задачи**: `BackgroundService` + `PeriodicTimer`, без внешних зависимостей. - **Ошибки**: собственный `Result` вместо исключений для управляемых сценариев; исключения — только для действительно исключительного. - **Логирование**: Serilog (`Serilog.AspNetCore`), `UseSerilogRequestLogging()` + `Enrich.FromLogContext()`. Секреты (пароли, JWT, `BotToken`) в логи не попадают. - **API-документация**: нативный OpenAPI (`Microsoft.AspNetCore.OpenApi`) + Scalar UI, без Swashbuckle. `/openapi/v1.json` используется фронтом для `pnpm gen:api` (openapi-typescript). `/scalar` — UI. - **Тесты**: xUnit + NSubstitute + Testcontainers (юнит-тесты домена/хендлеров, интеграционные — с реальным PostgreSQL через `Testcontainers.PostgreSql` + `WebApplicationFactory`). ## Frontend - **React 19 + Vite + TypeScript** — SPA, без SSR. - **Данные с сервера**: TanStack Query — кэш, инвалидация, фоновые рефетчи, статусы загрузки/ошибок. - **Роутинг**: TanStack Router — типобезопасный, интеграция с TanStack Query. - **UI**: shadcn/ui + Tailwind CSS v4 (компоненты на Radix), тёмная/светлая тема. Иконки — `lucide-react`. - **Клиентский стейт**: Zustand — только для авторизации и темы; серверный стейт — в TanStack Query. - **Формы**: react-hook-form + zod. - **Realtime**: `@microsoft/signalr` — подписки на события хаба обновляют кэш TanStack Query. - **Типы API**: `pnpm gen:api` гоняет `openapi-typescript` по `/openapi/v1.json` живого бэкенда → `shared/api/schema.gen.ts`. Фичи импортируют типы из руками написанного `shared/api/types.ts` (см. [frontend.md](frontend.md)) — даёт нормальные generic (`PagedList`) и понятные имена. - **QR-коды**: `qrcode.react`. - **i18n**: react-i18next, языки RU + EN (RU по умолчанию). Тексты — через ключи, не хардкод строк. ## Инфраструктура Единый образ приложения (REST + SignalR + Telegram-бот + статика SPA) + отдельный контейнер PostgreSQL. - **Multi-stage Dockerfile**: (1) `node` собирает фронт → `dist/`; (2) `dotnet sdk` публикует Api и копирует статику в `wwwroot`; (3) `aspnet` runtime запускает Api. Api раздаёт SPA (`UseStaticFiles` + fallback на `index.html`), фронт и бек — один origin. - **docker-compose**: `app` (единый образ) + `db` (PostgreSQL) с томом. - Отдельного nginx для статики нет — единый origin упрощает CORS/куки/деплой. - **TLS — внешний**: HTTPS терминирует внешний прокси/шлюз (nginx/Traefik/cloud LB) вне compose; `app` отдаёт HTTP и доверяет `X-Forwarded-*` через `ForwardedHeaders`. - **Миграции** — применяются автоматически на старте приложения. - **CI** — GitHub Actions: `dotnet build`/`test`, `pnpm build`/`lint`/`typecheck`. Без деплоя. - **Пакетный менеджер фронта**: pnpm. ## Ключевые решения по домену и поведению | Тема | Как сделано | | -------------------------- | --------------------------------------------------------------------------------- | | Ролей у пользователя | Ровно одна роль (квота = `MaxConfigs` роли) | | Секреты нод | ASP.NET Core Data Protection (шифрование at-rest, key-ring на томе) | | Тарифы/лимиты трафика | Не реализованы — конфиги без лимитов трафика/срока. Есть глобальная справочная цена за конфиг (`PricingSettings`, видна только админу) — без биллинг-логики | | i18n | RU + EN (react-i18next) | | Telegram-транспорт | Long polling | | Регистрация через Telegram | Поддержана (логин — Telegram `@username`/id, пароль генерируется и присылается в чат) | | История трафика | Простая таблица PostgreSQL (`TrafficSample`) + TTL-чистка (`TrafficRetentionService`) | | Логирование | Serilog (Console) | | Вход | По username (email не используется; SMTP не нужен) | | Восстановление пароля | Через привязанный Telegram (self-service); без привязки — сброс админом | | Регистрация | Открытая + гейт активации админом | | Конфиги в одном инбаунде | Разрешено несколько (ограничение — только общая квота роли) | | Данные ноды пользователю | Показываем только `DisplayName` + протокол; адрес/хост/порт скрыты | | Блокировка пользователя | Отключает все его конфиги в 3x-ui (`Disabled`); разблокировка — включает обратно | | Понижение роли | Грандфазеринг: существующие конфиги живут, новые нельзя до входа в квоту | | Скоуп Telegram-бота | Read-only по конфигам (создание/отзыв — на сайте); DM-уведомления юзеру | | Подписка | Агрегированная на юзера (`AppUser.SubscriptionToken`) + по конфигу | | Аудит | `AuditLog` (append-only): активация, блокировка, смена роли, отзыв, ноды/инбаунды | | Ротация конфига | `Rotate()` — перевыпуск UUID/ссылки, квоту не тратит (на случай утечки) | | Метка конфига | `Label` — пользователь именует конфиг («Мой телефон») | | Лимит устройств (`limitIp`) | Квота роли (`AppRole.MaxIpLimit`; -1 = без лимита), применяется только к новым клиентам в 3x-ui | | Самоудаление аккаунта | Отзыв всех активных конфигов в 3x-ui + удаление `AppUser` | | Удаление пользователя админом | `DELETE /api/admin/users/{id}` — отзыв всех конфигов в 3x-ui + удаление `AppUser`; себя удалить нельзя | | Вложения тикетов поддержки | Диск в контейнере (`IFileStorage`/`DiskFileStorage`, volume `ticket_uploads`) — не S3, GUID-имена файлов | | Заявки на роль из бота | Одобрение/отклонение полностью в Telegram (`rrq:*`); баг-репорты — только ссылка на сайт | | Версионирование API | Без версий (`/api` без `v1`) | | Подписка (заголовки) | `Subscription-Userinfo` (used/total/expire) + `profile-update-interval` | | Тема сайта | Светлая + тёмная (+ системная); выбор в localStorage | | Инструкции/приложения | Отдельная страница инструкций + каталог `ClientApp` (админ CRUD, юзер — по ОС) | | Реконсиляция с 3x-ui | `TrafficSyncService` молча пропускает ноду/клиента при недоступности или несовпадении, без активной реконсиляции (см. [architecture.md](architecture.md)) | Также реализовано: Identity lockout по неудачным входам; проверка квоты конфигов под `pg_advisory_xact_lock`; схема `ClientEmail = pnv_{userIdShort}_{rand}`. Явного анти-CSRF токена на refresh-cookie нет — обоснование в [architecture.md](architecture.md#безопасность) (`SameSite=Strict` + `HttpOnly` достаточно при мутациях только по Bearer-токену). Удаление ноды с активными конфигами **не блокируется** — известный пробел: `DeleteNodeCommandHandler` каскадно удаляет инбаунды ноды без проверки существующих `VpnConfig`. Email/SMTP в проекте не используются (вход по username, восстановление — через Telegram/админа).