Files
PnvPanel/docs/tech-stack.md
T
Leonid Pershin b05b76f32f
CI / Backend (build + test) (push) Failing after 1m28s
CI / Frontend (lint + typecheck + build) (push) Successful in 47s
Refactor messaging system to utilize LiteCqrs library
- Replaced instances of the previous messaging system with LiteCqrs across various application components, enhancing the CQRS implementation.
- Updated dependency injection to register LiteCqrs services and behaviors, streamlining command and query handling.
- Adjusted multiple command and query handlers to align with the new messaging framework, ensuring consistent functionality and improved maintainability.
- Added LiteCqrs package reference in the project file for better dependency management.
2026-07-24 04:16:38 +03:00

14 KiB
Raw Blame History

Tech Stack

Backend

  • Платформа: .NET 10, ASP.NET Core Web API (Minimal API).
  • Архитектура: Clean Architecture, 4 проекта — Domain / Application / Infrastructure / Api.
  • CQRS: через LiteCqrs.Net — собственную лёгкую CQRS-библиотеку (сосед-репозиторий, на время разработки подключён ProjectReference'ом; ещё не опубликован в NuGet), альтернативу MediatR с явным разделением Command/Query. ISender.Send() резолвит ICommandHandler<,>/IQueryHandler<,> из DI (dynamic-free кэшированный диспетчер) и прогоняет через IPipelineBehavior<,>: LoggingBehavior (готовый, из LiteCqrs.Behaviors), ValidationBehavior (FluentValidation, свой), RequireActivationBehavior (свой), UnitOfWorkBehavior (транзакция + SaveChangesAsync на команду, свой) — регистрация через AddLiteCqrs(...) в PnvPanel.Application/DependencyInjection.cs. Библиотека также даёт Notifications/pub-sub, exception behaviors и streaming-запросы — PnvPanel их пока не использует. Авторизация проверяется на уровне эндпоинта (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 — устройства MaxIpLimit, доступ к инбаундам Inbound.AllowedRoles, флаг BillingEnabled). Квота на число конфигов — на пользователе (AppUser.ConfigQuota), задаётся самообслуживаемым тарифом (Plan), не ролью; безлимит (-1) зарезервирован за ролью admin.
  • Активация пользователей: AppUser.IsActivated + ActivationRequest (с комментарием). Неактивированный не создаёт конфиги; решение принимает админ на сайте или в Telegram — одними и теми же CQRS-командами.
  • Сидинг из env: идемпотентный DbInitializer на старте — системные роли (admin/user), учётка админа и Telegram id админов. Пример — .env.example.
  • Realtime: SignalR — авто-транспорт (WebSocket→SSE→long-poll), группы/пользователи, JWT-авторизация хабов.
  • Telegram-бот: Telegram.Bot, хостится в процессе Api как BackgroundService (long polling) и вызывает те же CQRS-хендлеры, что и REST. Passwordless-вход выпускает те же JWT/refresh, что и веб. Детали — telegram-bot.md.
  • Фоновые задачи: BackgroundService + PeriodicTimer, без внешних зависимостей.
  • Ошибки: собственный Result<T> вместо исключений для управляемых сценариев; исключения — только для действительно исключительного.
  • Логирование: 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<Program>).

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) — даёт нормальные generic (PagedList<T>) и понятные имена.
  • 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, редактирует только admin) — ставка ₽/конфиг/месяц по периодам 3/6/12 мес
Биллинг (подписка по сроку) Реализован, но включается per-роль (AppRole.BillingEnabled, недоступен для admin) — см. domain-model.md. Не биллинг-система по умолчанию: роль без флага живёт без ограничений по сроку, как раньше
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)

Также реализовано: Identity lockout по неудачным входам; проверка квоты конфигов под pg_advisory_xact_lock; схема ClientEmail = pnv_{userIdShort}_{rand}. Явного анти-CSRF токена на refresh-cookie нет — обоснование в architecture.md (SameSite=Strict

  • HttpOnly достаточно при мутациях только по Bearer-токену). Удаление ноды с активными конфигами не блокируется — известный пробел: DeleteNodeCommandHandler каскадно удаляет инбаунды ноды без проверки существующих VpnConfig.

Email/SMTP в проекте не используются (вход по username, восстановление — через Telegram/админа).