Files
PnvPanel/docs/tech-stack.md
T
Leonid Pershin fad03c2834
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 34s
Enhance user plan management and update related endpoints
- Added new configuration options for user plans in `.env.example`, including `Plans__MaxCustomConfigCount` and `Plans__MinCustomConfigCount`.
- Introduced `MapPlanEndpoints` in `Program.cs` to handle plan-related API routes.
- Implemented `SetUserPlan` endpoint in `RoleEndpoints` to allow admins to assign plans to users.
- Removed deprecated role request approval endpoints from `AdminSupportEndpoints`.
- Updated `ITelegramNotifier` and related classes to reflect changes in role request handling and payment notifications.
- Refactored role management commands to remove `MaxConfigs` and focus on `MaxIpLimit` and billing settings.
- Enhanced billing request handling to accommodate plan changes instead of role changes.
- Updated various interfaces and command handlers to support new plan management features.
2026-07-23 22:52:20 +03:00

118 lines
13 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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` — устройства `MaxIpLimit`, доступ к инбаундам
`Inbound.AllowedRoles`, флаг `BillingEnabled`). Квота на число конфигов — на пользователе
(`AppUser.ConfigQuota`), задаётся самообслуживаемым тарифом (`Plan`), не ролью; безлимит (`-1`)
зарезервирован за ролью `admin`.
- **Активация пользователей**: `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<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](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](domain-model.md#billing--подписка-по-сроку). Не биллинг-система по умолчанию: роль без флага живёт без ограничений по сроку, как раньше |
| 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/админа).