Files
PnvPanel/docs/tech-stack.md
T
Leonid Pershin ae379f8e0f
CI / Backend (build + test) (push) Successful in 1m19s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s
Implement pricing management functionality and update related components
- Added new endpoints for managing global pricing settings, including retrieval and updates for `PricePerConfigPerQuarter` and `PricePerConfigPerYear`.
- Updated `RoleService` and related commands to remove pricing fields from role management, ensuring a clear separation between role configurations and global pricing.
- Enhanced the `FactoryResetCommandHandler` to include seeding of pricing settings during a factory reset.
- Modified frontend components to support new pricing settings, including forms for creating and updating pricing information.
- Updated API documentation to reflect changes in pricing management endpoints and their expected request/response formats.
- Adjusted tests to ensure proper coverage for new pricing functionalities and their integration with existing role management features.
2026-07-18 19:34:29 +03:00

115 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.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<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`, видна только админу) — без биллинг-логики |
| 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/админа).