Implement role and user management enhancements
- Added MaxIpLimit to roles, allowing for the configuration of simultaneous IP limits for users. - Updated role creation and update commands to include MaxIpLimit, ensuring proper handling in the application logic. - Enhanced user management by introducing a DELETE endpoint for user accounts, with appropriate checks to prevent self-deletion. - Updated documentation to reflect changes in role and user management, clarifying the new IP limit functionality and user deletion process. - Adjusted related tests to cover new functionality and ensure robust validation of role and user management features.
This commit is contained in:
@@ -4,151 +4,129 @@
|
||||
|
||||
## Что это
|
||||
|
||||
**PnvPanel** — self-service портал для VPN-конфигураций. Пользователи сами создают себе конфиги
|
||||
(VLESS/VMess/Trojan/Shadowsocks), админ управляет серверами и пользователями. Есть **Telegram-бот**
|
||||
(ссылка на сайт, просмотр конфигов, passwordless-вход через привязку Telegram). Бэкенд оркестрирует
|
||||
панели **3x-ui** через библиотеку [`ThreeXui.Net`](https://github.com/mrleo1nid/ThreeXui.Net) и
|
||||
хранит свою проекцию домена в PostgreSQL. Живые обновления — по SignalR. Приложение (фронт + бек +
|
||||
бот) поставляется **единым Docker-образом**; PostgreSQL — отдельным контейнером в compose.
|
||||
**PnvPanel** — self-service портал для VPN-конфигураций (VLESS/VMess/Trojan/Shadowsocks): пользователи
|
||||
сами создают конфиги, админ управляет серверами и пользователями. Есть **Telegram-бот** (ссылка на
|
||||
сайт, просмотр конфигов, passwordless-вход). Бэкенд оркестрирует панели **3x-ui** через
|
||||
[`ThreeXui.Net`](https://github.com/mrleo1nid/ThreeXui.Net) и хранит проекцию домена в PostgreSQL.
|
||||
Живые обновления — SignalR. Поставка — **единый Docker-образ** (фронт+бек+бот) + PostgreSQL в compose.
|
||||
|
||||
> Бэкенд и фронтенд полностью собраны и покрыты тестами (134 бэкенд-теста), единый Docker-образ и
|
||||
> docker-compose стек проверены живьём. Осознанно не реализовано: тарифы, лимиты трафика/срока на
|
||||
> конфиг, полное самообслуживание в боте — см. [tech-stack.md](docs/tech-stack.md).
|
||||
> Собрано и покрыто тестами, единый образ и compose-стек проверены живьём. Осознанно не реализовано:
|
||||
> тарифы, лимиты трафика/срока на конфиг, полное самообслуживание в боте — см. [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)
|
||||
[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 (**собственный тонкий
|
||||
диспетчер**, без MediatR), EF Core 10 + Npgsql (PostgreSQL), ASP.NET Core Identity + JWT, SignalR,
|
||||
FluentValidation, **Serilog** (логирование). Маппинг DTO — вручную (`FromDomain(...)`), Mapster в
|
||||
проект не попал. OpenAPI — нативный `Microsoft.AspNetCore.OpenApi` + Scalar UI, без Swashbuckle.
|
||||
- **Frontend**: React 19 + Vite + TypeScript, TanStack Query/Router, shadcn-стиль поверх Radix +
|
||||
Tailwind CSS v4, Zustand (только auth-стор), react-hook-form + zod, @microsoft/signalr. Пакетный
|
||||
менеджер — pnpm, линтер — oxlint. `recharts`/`@tanstack/react-table` установлены, но не
|
||||
используются (статистика — карточками, таблицы — руками).
|
||||
- **Telegram**: Telegram.Bot, бот как `BackgroundService` **в процессе Api** (long polling).
|
||||
- **Инфра**: единый Docker-образ (API + бот + статика SPA) + PostgreSQL в docker-compose.
|
||||
диспетчер**, без MediatR), 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`,
|
||||
фабричные/поведенческие методы). Никаких 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.
|
||||
|
||||
Обязательно:
|
||||
- CQRS: команды меняют состояние и идут в транзакции (UnitOfWorkBehavior); запросы только читают
|
||||
(`AsNoTracking` + проекция в DTO). Диспетчер — **собственный** (`ISender`/`ICommandHandler`/
|
||||
`IQueryHandler`, регистрация хендлеров через DI), без внешних CQRS-библиотек.
|
||||
- Управляемые ошибки — через `Result<T>`, не исключениями. Исключения — только для исключительного.
|
||||
- Команды меняют состояние в транзакции (`UnitOfWorkBehavior`); запросы только читают (`AsNoTracking` +
|
||||
проекция в DTO). Диспетчер — **собственный** (`ISender`/`ICommandHandler`/`IQueryHandler`, DI-регистрация).
|
||||
- Управляемые ошибки — через `Result<T>`, не исключениями (исключения только для исключительного).
|
||||
- Валидация — FluentValidation через `ValidationBehavior`; хендлер не перепроверяет формат ввода.
|
||||
- Всё I/O асинхронно, `CancellationToken` пробрасывается до EF/HTTP. Никаких `.Result`/`.Wait()`.
|
||||
- Nullable reference types включены; предупреждения анализаторов не игнорировать.
|
||||
|
||||
## Интеграция с 3x-ui
|
||||
|
||||
- Только через порт `IXuiPanelGateway`. `ThreeXui.Net` регистрируется на один `BaseAddress`, а нод
|
||||
много → гейтвей держит **клиента per-node** (кэш по `NodeId`), создавая его из расшифрованных
|
||||
`NodeCredentials`. Детали — в [architecture.md](docs/architecture.md#интеграция-с-3x-ui-threexuinet).
|
||||
- Пароли нод **шифруются at-rest** (`ISecretProtector`), расшифровка только внутри Infrastructure,
|
||||
никогда не в логах/ответах API.
|
||||
- Только через порт `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 наружу.
|
||||
- Операции с 3x-ui идемпотентны; при частичном сбое (клиент создан в панели, но упала БД) — компенсация.
|
||||
- Операции идемпотентны; при частичном сбое (клиент создан в панели, упала БД) — компенсация.
|
||||
|
||||
## Роли, активация, сидинг
|
||||
## Домен: роли, активация, конфиги
|
||||
|
||||
- **Роли динамические**: `AppRole : IdentityRole<Guid>` + поле `MaxConfigs` (квота на число конфигов).
|
||||
Квота — **на роли, а не на `Plan`**. **У пользователя ровно одна роль**; квота = `MaxConfigs` его
|
||||
роли (`admin` — без лимита). Системные роли (`admin`/`user`) не удалять/переименовывать.
|
||||
- **Активация**: новый пользователь `IsActivated = false`, роль `user`. Конфиги может создавать
|
||||
только активированный. `ActivationRequest` (с комментарием заявителя) одобряет админ на сайте
|
||||
**или** в Telegram — одними и теми же командами (`ApproveActivationCommand`/`RejectActivationCommand`).
|
||||
- **Инбаунды по ролям**: `Inbound.AllowedRoles` (M:N). При создании конфига доменный инвариант
|
||||
проверяет: активирован + под квотой роли + роль входит в `AllowedRoles` инбаунда + нода включена.
|
||||
Проверку квоты делать **в транзакции** (гонки параллельных созданий).
|
||||
- **Понижение роли — грандфазеринг**: смена на меньшую квоту разрешена; лишние конфиги не отзываем,
|
||||
но новые нельзя до входа в квоту.
|
||||
- **Блокировка** (`AppUser.IsBlocked`): вход запрещён + все конфиги `Disabled` (отключить клиентов
|
||||
в 3x-ui); разблокировка — обратно. Действие в `AuditLog`.
|
||||
- **Аудит**: значимые действия (активация, блок, смена роли, отзыв, ноды/инбаунды) писать в `AuditLog`
|
||||
Полная модель — [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`; конфиги создаёт только
|
||||
активированный. `ActivationRequest` одобряет админ на сайте или в Telegram — одними командами.
|
||||
- **Инбаунды по ролям** (`Inbound.AllowedRoles`, M:N): создание конфига проверяет активацию + квоту роли
|
||||
(в транзакции — гонки параллельных созданий) + `AllowedRoles` + включённость ноды.
|
||||
- **Блокировка** (`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).
|
||||
- **Подписка**: агрегированная на юзера (`AppUser.SubscriptionToken`, все активные конфиги) + по конфигу.
|
||||
- **Ротация конфига** (`Rotate()`): новый UUID/ссылка, квоту не тратит. **Бот — read-only** по конфигам.
|
||||
- **Конфиг**: пользователь задаёт метку (`Label`), может редактировать. Лимит устройств (`limitIp` в 3x-ui) панелью не управляется — более сложная настройка, задаётся при необходимости администратором ноды напрямую в 3x-ui.
|
||||
- **Самоудаление аккаунта** (`DELETE /api/auth/me`): отзыв всех конфигов + удаление данных, аудит анонимизируется.
|
||||
- **API без версионирования** (`/api` без `v1`). Подписка отдаёт `Subscription-Userinfo`.
|
||||
- **Тема**: светлая/тёмная/системная (Tailwind `dark`, выбор в localStorage).
|
||||
- **Инструкции + приложения**: отдельная страница инструкций; каталог `ClientApp` (админ CRUD:
|
||||
название/ссылка/ОС/порядок/вкл), пользователю `GET /api/apps` отдаётся сгруппированным по ОС.
|
||||
- **Вход — по `UserName`** (email в системе не используется вовсе; SMTP не нужен).
|
||||
Восстановление пароля: через привязанный Telegram (self-service), без привязки — сброс админом
|
||||
(`ResetUserPasswordCommand`). Пока Telegram не привязан — UI настойчиво предлагает его привязать.
|
||||
- **Сидинг из env**: идемпотентный `DbInitializer` на старте создаёт системные роли и учётку админа
|
||||
(`AdminSeed__Username`/`AdminSeed__Password`) из переменных окружения; каталог приложений `ClientApp`
|
||||
(если пуст) — из [`seed/client-apps.json`](seed/client-apps.json). Единый источник примера env —
|
||||
[`.env.example`](.env.example); при добавлении новой настройки обновляй и его. Секреты (пароль
|
||||
админа, JWT-ключ, `Telegram__BotToken`) — только через env/secret-store.
|
||||
- Telegram id админов — **отдельно от сидинга**, `Telegram__AdminTelegramUserIds` (через запятую),
|
||||
читается `TelegramOptions` напрямую при каждой проверке, не пишется в БД. Именно он авторизует
|
||||
админ-кнопки в боте и адресует уведомления о запросах активации. Seed-админ **не** привязывается к
|
||||
Telegram автоматически — привязка делается вручную в UI, как у любого пользователя.
|
||||
- **Ротация конфига** (`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-бот
|
||||
|
||||
- Бот — **presentation-адаптер**, не бизнес-слой. Хостится в процессе Api (`TelegramBotHostedService`,
|
||||
long polling). Хендлеры апдейтов вызывают **те же** CQRS-команды/запросы через собственный `ISender`
|
||||
(`GetMyConfigsQuery`, `LinkTelegramCommand`, `ApproveTelegramLoginCommand`, ...).
|
||||
- `Telegram.Bot` не проникает в Application/Domain — только в `Api/Telegram/`.
|
||||
- Passwordless-вход выпускает **те же** JWT/refresh, что и обычный логин. Требует привязки Telegram —
|
||||
либо привязка существующего аккаунта (с сайта), либо регистрация прямо из бота (`RegisterViaTelegramCommand`):
|
||||
логин — Telegram `@username`, при отсутствии/занятости — Telegram id; пароль генерируется и
|
||||
присылается в чат один раз (логин можно сменить позже в Настройках, `ChangeUserNameCommand`).
|
||||
Новый аккаунт получает роль `user` и `IsActivated = false` — активация нужна как обычно.
|
||||
- Токены привязки/входа: короткоживущие, одноразовые, высокоэнтропийные. `Telegram:BotToken` — секрет,
|
||||
не логировать. Панель должна работать и без бота (если токен не задан — бот просто не стартует).
|
||||
- Детали флоу — [telegram-bot.md](docs/telegram-bot.md).
|
||||
Детали флоу — [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 — внешний** (прокси/шлюз вне compose); `app` отдаёт HTTP + доверяет `X-Forwarded-*` через
|
||||
`ForwardedHeaders` (иначе Secure-cookie/схема за прокси сломаются). Свой nginx/Caddy не добавляй.
|
||||
- **Миграции** применяются авто на старте. **CI** (GitHub Actions) — только build/test, без деплоя.
|
||||
- Один образ: 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). Кратко:
|
||||
Полный список — [backend-conventions.md](docs/backend-conventions.md). Кратко:
|
||||
|
||||
- Команды `<Verb><Noun>Command`, запросы `<Get/List><Noun>Query`, + `Handler`/`Validator` (валидатор —
|
||||
не для каждой команды, только где есть что проверить). Application DTO — суффикс `Dto`
|
||||
(`FromDomain(...)` конвертирует из сущности); тела запросов Api-слоя — суффикс `Body`; тела ответов,
|
||||
которых нет как Application DTO — суффикс `ResponseDto`.
|
||||
- Application организована **по фичам** (feature folders) внутри слоёв.
|
||||
- Один публичный тип на файл, имя файла = имя типа (кроме вспомогательных `Body`/`ResponseDto`
|
||||
records — они живут в том же файле, что и класс эндпоинтов). Async-методы — суффикс `Async` + `CancellationToken`.
|
||||
- Секреты не логировать; логи — Serilog (`UseSerilogRequestLogging` + `Enrich.FromLogContext()`).
|
||||
Явного обогащения `UserId`/`NodeId`/`ConfigId`/`CorrelationId` пока нет — не полагайся на него при
|
||||
расследовании, пока не добавлено.
|
||||
- Ошибки API — единый `application/problem+json` (без Swashbuckle — нативный `Microsoft.AspNetCore.OpenApi`).
|
||||
- `<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`.
|
||||
|
||||
## Команды
|
||||
|
||||
@@ -177,19 +155,17 @@ docker compose up -d # api + postgres (+ web)
|
||||
```
|
||||
|
||||
> Окружение: Windows, основная оболочка — **PowerShell**. Для POSIX-скриптов есть Bash-инструмент.
|
||||
> Пути — с учётом Windows.
|
||||
|
||||
## Ключевые решения
|
||||
|
||||
См. [tech-stack.md](docs/tech-stack.md#ключевые-решения-по-домену-и-поведению): CQRS —
|
||||
**собственный диспетчер** (не MediatR); **одна роль** на пользователя; секреты нод —
|
||||
**ASP.NET Data Protection**; тарифы `Plan` не реализованы (нет лимитов трафика/срока на конфиг);
|
||||
i18n — **RU+EN** (react-i18next); Telegram — **long polling**, только **привязка** (не signup);
|
||||
история трафика — **простая таблица + TTL**; логирование — **Serilog**.
|
||||
См. [tech-stack.md](docs/tech-stack.md#ключевые-решения-по-домену-и-поведению): CQRS — собственный
|
||||
диспетчер (не MediatR); одна роль на пользователя; секреты нод — ASP.NET Data Protection; тарифы `Plan`
|
||||
не реализованы; i18n — RU+EN (react-i18next); Telegram — long polling, только привязка (не signup);
|
||||
история трафика — простая таблица + TTL; логирование — Serilog.
|
||||
|
||||
## Рабочие принципы
|
||||
|
||||
- Не начинай крупную реализацию без сверки с доками и, при неоднозначности, без вопроса пользователю.
|
||||
- Соблюдай границы слоёв — это главный инвариант проекта. Нарушение = ошибка ревью.
|
||||
- Соблюдай границы слоёв — главный инвариант проекта. Нарушение = ошибка ревью.
|
||||
- Обновляй документацию вместе с кодом. Не коммить и не пуши без явной просьбы.
|
||||
- Отвечай пользователю на русском (язык общения в проекте — русский).
|
||||
- Отвечай пользователю на русском.
|
||||
|
||||
Reference in New Issue
Block a user