- Introduced a new support ticket system allowing users to submit bug reports and role requests. - Implemented endpoints for creating, updating, and managing support tickets, including file attachments. - Enhanced Telegram bot integration to handle role requests directly within the bot, enabling admins to approve or reject requests without accessing the website. - Updated database schema to include support ticket entities and their relationships. - Improved API documentation to reflect new support ticket endpoints and their usage. - Added necessary localization for support ticket features in both Russian and English.
115 lines
13 KiB
Markdown
115 lines
13 KiB
Markdown
# 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 на томе) |
|
||
| Тарифы/лимиты трафика | Не реализованы — конфиги без лимитов трафика/срока |
|
||
| 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/админа).
|