Files
Leonid Pershin 9925968e22
CI / Backend (build + test) (push) Failing after 1m23s
CI / Frontend (lint + typecheck + build) (push) Successful in 36s
Update LiteCqrs integration and documentation
- Replaced references to local project connections with the NuGet package for LiteCqrs in multiple documentation files, ensuring clarity on dependency management.
- Updated architecture and backend conventions documentation to reflect the current state of the LiteCqrs library as a NuGet package, enhancing consistency across the project.
- Improved descriptions of CQRS implementation and command/query handling in the tech stack documentation, providing clearer guidance for developers.
2026-07-24 04:18:03 +03:00

192 lines
17 KiB
Markdown
Raw Permalink 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.
# CLAUDE.md
Инструкции для Claude Code при работе в этом репозитории.
## Что это
**PnvPanel** — self-service портал для VPN-конфигураций (VLESS/VMess/Trojan/Shadowsocks): пользователи
сами создают конфиги, админ управляет серверами и пользователями. Есть **Telegram-бот** (ссылка на
сайт, просмотр конфигов, passwordless-вход). Бэкенд оркестрирует панели **3x-ui** через
[`ThreeXui.Net`](https://github.com/mrleo1nid/ThreeXui.Net), CQRS — через собственную библиотеку
[`LiteCqrs.Net`](https://github.com/mrleo1nid/LiteCqrs.Net) (NuGet-пакет), и хранит проекцию домена
в PostgreSQL.
Живые обновления — SignalR. Поставка — **единый Docker-образ** (фронт+бек+бот) + PostgreSQL в compose.
> Собрано и покрыто тестами, единый образ и compose-стек проверены живьём. Есть опциональный биллинг
> (подписка по сроку, per-роль). Осознанно не реализовано: лимиты трафика на конфиг, полное
> самообслуживание в боте — см. [tech-stack.md](docs/tech-stack.md).
## Документация (single source of truth)
Прежде чем менять архитектуру или добавлять фичу — свериться с [`docs/`](docs/README.md):
[Vision](docs/vision.md) · [Architecture](docs/architecture.md) · [Domain Model](docs/domain-model.md) ·
[Tech Stack](docs/tech-stack.md) · [Backend Conventions](docs/backend-conventions.md) ·
[Frontend](docs/frontend.md) · [Telegram Bot](docs/telegram-bot.md) · [API Design](docs/api-design.md)
**Держи доки в синхроне с кодом.** Меняешь контракт/архитектуру — обнови соответствующий док в том же изменении.
## Стек
- **Backend**: C# / .NET 10, ASP.NET Core Web API, Clean Architecture, CQRS (через
[`LiteCqrs.Net`](https://github.com/mrleo1nid/LiteCqrs.Net) — собственная лёгкая
CQRS-библиотека, альтернатива MediatR с явным разделением Command/Query), EF Core 10 + Npgsql,
ASP.NET Core Identity + JWT, SignalR, FluentValidation,
Serilog. Маппинг DTO вручную (`FromDomain(...)`, без Mapster). OpenAPI — нативный
`Microsoft.AspNetCore.OpenApi` + Scalar UI (без Swashbuckle).
- **Frontend**: React 19 + Vite + TS, TanStack Query/Router, shadcn-стиль поверх Radix + Tailwind v4,
Zustand (только auth), react-hook-form + zod, @microsoft/signalr. pnpm, oxlint.
- **Telegram**: Telegram.Bot как `BackgroundService` **в процессе Api** (long polling).
- **Инфра**: единый Docker-образ (API+бот+статика SPA) + PostgreSQL в docker-compose.
## Архитектура — жёсткие правила
Слои и направление зависимостей: **Api → Infrastructure → Application → Domain** (внутрь).
- **Domain** — без внешних зависимостей. Rich model: инварианты в сущностях (приватные сеттеры,
фабричные/поведенческие методы). Никаких EF/HTTP/Identity.
- **Application** — CQRS-хендлеры, DTO, валидаторы, **порты** (интерфейсы). Зависит только от Domain —
никаких `Npgsql`/`SignalR`/`ThreeXui.Net`, только их интерфейсы (`IAppDbContext`, `IXuiPanelGateway`,
`IRealtimeNotifier`, `ISecretProtector`, `ICurrentUser`, ...).
- **Infrastructure** — реализации портов: EF Core, Identity/JWT, `XuiPanelGateway`, SignalR-пуш,
фоновые сервисы, шифрование секретов.
- **Api** — Minimal API эндпоинты (по фичам), SignalR-хабы, middleware, DI composition root.
Обязательно:
- Команды меняют состояние в транзакции (`UnitOfWorkBehavior`); запросы только читают (`AsNoTracking` +
проекция в DTO). Диспетчер — **собственный** (`ISender`/`ICommandHandler`/`IQueryHandler`, DI-регистрация).
- Управляемые ошибки — через `Result<T>`, не исключениями (исключения только для исключительного).
- Валидация — FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода.
- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP. Никаких `.Result`/`.Wait()`.
- Nullable reference types включены; предупреждения анализаторов не игнорировать.
## Интеграция с 3x-ui
- Только через порт `IXuiPanelGateway`. Один `BaseAddress` в `ThreeXui.Net`, а нод много → гейтвей
держит **клиента per-node** (кэш по `NodeId`) из расшифрованных `NodeCredentials`. Детали —
[architecture.md](docs/architecture.md#интеграция-с-3x-ui-threexuinet).
- Пароли нод **шифруются at-rest** (`ISecretProtector`), расшифровка только в Infrastructure, никогда в логах/ответах.
- Недоступность ноды → `Result.Failure`/`NodeStatus.Offline`, не 500 наружу.
- Операции идемпотентны; при частичном сбое (клиент создан в панели, упала БД) — компенсация.
## Домен: роли, активация, конфиги
Полная модель — [domain-model.md](docs/domain-model.md). Ключевые инварианты:
- **Роли динамические** (`AppRole`), квоты на роли (не на `Plan`): `MaxConfigs` (число конфигов),
`MaxIpLimit` (лимит одновременных IP клиента в 3x-ui, `limitIp`); -1 = без лимита. У пользователя
ровно одна роль; `admin` — без лимитов. Системные роли `admin`/`user` не удалять/переименовывать.
Понижение роли — грандфазеринг (лишние конфиги не отзываются, новые блокируются до входа в квоту).
- **`limitIp`** выставляется автоматически по `MaxIpLimit` роли при создании клиента (`Create`/`Rotate`);
панель не даёт настраивать его per-конфиг и не трогает уже созданных клиентов при смене роли/квоты.
- **Активация**: новый пользователь `IsActivated=false`, роль `user`; неактивированному недоступны
конфиги (создание/просмотр/редактирование/ротация/отзыв/ссылка/подписка), новости и каталог
приложений — единая проверка `RequireActivationBehavior` по маркеру `IRequiresActivation` (не
разбросанные `if` в хендлерах). На фронте до активации доступны только дашборд (форма запроса
активации) и настройки аккаунта. `ActivationRequest` одобряет админ на сайте или в Telegram —
одними командами.
- **Инбаунды по ролям** (`Inbound.AllowedRoles`, M:N): создание конфига проверяет активацию + квоту роли
(в транзакции — гонки параллельных созданий) + `AllowedRoles` + включённость ноды.
- **Поддержка** (`SupportTicket`, доступна только активированным): баг-репорт/предложение (свободная
форма + вложения-картинки, диск-хранилище `IFileStorage`) либо заявка на роль (существующая роль,
кроме `admin`, либо параметры новой). `Open → Resolved → [Reopen]`, `Closed` — финал без возврата.
Одобрение заявки на роль создаёт/назначает роль автоматически; полностью решается и в Telegram
(инлайн-кнопки), баг-репорты — только уведомление-ссылка на сайт.
- **Блокировка** (`AppUser.IsBlocked`): вход запрещён + все конфиги `Disabled` в 3x-ui; в `AuditLog`.
- **Удаление пользователя** — свой аккаунт (`DELETE /api/auth/me`) или админом
(`DELETE /api/admin/users/{id}`, себя удалить нельзя): отзыв всех конфигов в 3x-ui, затем `AppUser`;
админский путь дополнительно пишет `AuditLog` (`UserDeleted`) и шлёт Telegram-DM.
- **Аудит**: значимые действия (активация/блок/роль/отзыв/ноды/инбаунды/удаление) — `AuditLog`
(append-only, источник Web/Telegram/System).
- **Биллинг** (`AppRole.BillingEnabled`, недоступен для `admin`): пользователь оформляет
`PaymentRequest` на 3/6/12 мес (сумма — по `PricingSettings`, заморожена на заявке), админ
подтверждает/отклоняет на сайте или в Telegram (`pay:*`). Пока заявка `AwaitingConfirmation`
конфиги не гасятся, даже если срок истёк (не по вине пользователя, что админ не успел). Просрочка
без заявки → `VpnConfig.Suspend()` (статус `Expired`, отдельно от `Disable()`/блокировки админом) —
см. [domain-model.md](docs/domain-model.md#billing--подписка-по-сроку).
- **Ротация конфига** (`Rotate()`) — новый UUID/ссылка, квоту не тратит. **Бот read-only** по конфигам.
- **Подписка**: агрегированная (`AppUser.SubscriptionToken`) + по конфигу. API без версионирования
(`/api`, без `v1`); подписка отдаёт `Subscription-Userinfo`.
- **Вход по `UserName`** (email не используется). Восстановление пароля — через привязанный Telegram
(self-service) либо сбросом админом (`ResetUserPasswordCommand`); без привязки UI настойчиво предлагает привязать.
- **Сидинг**: идемпотентный `DbInitializer` создаёт системные роли + админа из env
(`AdminSeed__Username`/`Password`); каталог `ClientApp` — из [`seed/client-apps.json`](seed/client-apps.json).
Источник примера env — [`.env.example`](.env.example), обновляй при новых настройках. Секреты — только через env.
`Telegram__AdminTelegramUserIds` — отдельно от сидинга, не пишется в БД, читается напрямую из `TelegramOptions`.
## Telegram-бот
Детали флоу — [telegram-bot.md](docs/telegram-bot.md).
- Presentation-адаптер, не бизнес-слой: `TelegramBotHostedService` (long polling) вызывает **те же**
CQRS-команды через `ISender`. `Telegram.Bot` не проникает в Application/Domain — только `Api/Telegram/`.
- Passwordless-вход выпускает те же JWT/refresh, что и обычный логин; требует привязки Telegram (с сайта)
либо регистрации прямо из бота (`RegisterViaTelegramCommand` — логин `@username`/id, пароль генерируется
и приходит в чат один раз). Новый аккаунт — роль `user`, `IsActivated=false`, активация как обычно.
- Токены привязки/входа — короткоживущие одноразовые. `Telegram:BotToken` — секрет, не логировать.
Без токена бот просто не стартует — панель работает и без него.
## Единый контейнер
- Один образ: Api раздаёт REST (`/api`), SignalR (`/hubs`), хостит бота и статику SPA из `wwwroot`
(fallback на `index.html`). Один origin, база API — относительный `/api`.
- Multi-stage Dockerfile: node (фронт) → dotnet sdk (publish + копирование в `wwwroot`) → aspnet runtime.
- docker-compose: `app` + `db` (PostgreSQL); в dev — Vite-прокси `/api`,`/hubs` на бэк.
- Не вводи отдельный nginx-контейнер для статики без явной просьбы — ломает требование единого контейнера.
- **TLS — внешний**; `app` отдаёт HTTP + доверяет `X-Forwarded-*` (`ForwardedHeaders`). Свой nginx/Caddy не добавляй.
- Миграции применяются авто на старте. CI (GitHub Actions) — только build/test, без деплоя.
## Соглашения по коду
Полный список — [backend-conventions.md](docs/backend-conventions.md). Кратко:
- `<Verb><Noun>Command`/`<Get|List><Noun>Query` + `Handler`/`Validator` (валидатор — где есть что
проверить). DTO — суффикс `Dto`; тела запросов Api — `Body`; тела ответов без Application DTO —
`ResponseDto`. Application — по фичам (feature folders).
- Один публичный тип на файл = имя файла (кроме `Body`/`ResponseDto` в файле эндпоинтов).
Async-методы — суффикс `Async` + `CancellationToken`.
- Секреты не логировать; логи — Serilog. Явного обогащения `UserId`/`NodeId`/`ConfigId`/`CorrelationId`
пока нет — не полагайся на него при расследовании.
- Ошибки API — единый `application/problem+json`.
## Команды
Backend (из `backend/`):
```bash
dotnet build
dotnet test
dotnet run --project src/PnvPanel.Api
dotnet ef migrations add <Name> --project src/PnvPanel.Infrastructure --startup-project src/PnvPanel.Api
dotnet ef database update --project src/PnvPanel.Infrastructure --startup-project src/PnvPanel.Api
dotnet format
```
Frontend (из `frontend/`):
```bash
pnpm install
pnpm dev
pnpm build
pnpm lint && pnpm typecheck
pnpm gen:api # типы из OpenAPI-схемы бэкенда
```
Инфраструктура:
```bash
docker compose up -d # api + postgres (+ web)
```
> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент.
## Ключевые решения
См. [tech-stack.md](docs/tech-stack.md#ключевые-решения-по-домену-и-поведению): CQRS — через
LiteCqrs.Net (собственная библиотека, не MediatR); одна роль на пользователя; секреты нод — ASP.NET Data Protection; биллинг
опционален per-роль (недоступен для `admin`); i18n — RU+EN (react-i18next); Telegram — long polling,
только привязка (не signup); история трафика — простая таблица + TTL; логирование — Serilog.
## Рабочие принципы
- Не начинай крупную реализацию без сверки с доками и, при неоднозначности, без вопроса пользователю.
- Соблюдай границы слоёв — главный инвариант проекта. Нарушение = ошибка ревью.
- Обновляй документацию вместе с кодом. Не коммить и не пуши без явной просьбы.
- Отвечай пользователю на русском.