Files
PnvPanel/docs/tech-stack.md
T
Leonid Pershin b2ae358250
CI / Backend (build + test) (push) Successful in 1m22s
CI / Frontend (lint + typecheck + build) (push) Successful in 33s
Implement billing functionality and enhance role management
- Introduced billing capabilities, allowing users to request payments for subscription periods (3/6/12 months) with admin approval via Telegram.
- Updated role management to include a `BillingEnabled` property, preventing billing for admin roles.
- Enhanced the `CreateRoleCommand` and `UpdateRoleCommand` to accept billing parameters, ensuring proper handling during role creation and updates.
- Added new endpoints for billing management and integrated billing checks into VPN config creation to enforce payment requirements.
- Updated related services, models, and tests to support the new billing features, ensuring comprehensive coverage and functionality.
- Enhanced documentation to reflect the new billing processes and role management changes.
2026-07-19 01:38:16 +03:00

13 KiB
Raw Blame History

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.
  • 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/админа).