- 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.
13 KiB
13 KiB
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)aspnetruntime запускает Api. Api раздаёт SPA (UseStaticFiles- fallback на
index.html), фронт и бек — один origin.
- fallback на
- 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/админа).