Refactor environment configuration and update documentation for MVP status
- Removed deprecated Telegram user ID configuration from `.env.example` and added a new setting for admin Telegram user IDs. - Updated `CLAUDE.md` to reflect the current MVP status, detailing completed features and testing coverage. - Enhanced `README.md` with quick start instructions for Docker setup and clarified project status. - Revised API design documentation to include updated error handling and request/response structures. - Improved frontend documentation to outline the project structure and technologies used.
This commit is contained in:
+150
-121
@@ -1,181 +1,210 @@
|
||||
# API Design
|
||||
|
||||
REST поверх HTTP/JSON, авторизация — `Authorization: Bearer <access-token>` (кроме публичных).
|
||||
Ошибки — `application/problem+json` (`ProblemDetails`). Пагинация — `?page=&pageSize=`,
|
||||
ответ `PagedList<T>` (`items`, `total`, `page`, `pageSize`). Все даты — ISO-8601 UTC.
|
||||
REST поверх HTTP/JSON, авторизация — `Authorization: Bearer <access-token>` (кроме публичных
|
||||
эндпоинтов). Ошибки — `application/problem+json`. Пагинация — `?page=&pageSize=`, ответ `PagedList<T>`
|
||||
(`items`, `total`, `page`, `pageSize`) — используется не везде, см. таблицы ниже. Все даты — ISO-8601 UTC.
|
||||
Тела запросов/ответов — camelCase JSON; енумы сериализуются строками (`"Active"`, не `0`).
|
||||
|
||||
Базовый префикс: `/api` (**без версионирования в MVP** — единый фронт+бек; версии введём при
|
||||
необходимости). Ниже — контракт MVP (может уточняться при реализации).
|
||||
Базовый префикс: `/api` (**без версионирования в MVP**). Схема генерируется нативным
|
||||
`Microsoft.AspNetCore.OpenApi` (`/openapi/v1.json`) и Scalar UI (`/scalar`) — каждый эндпоинт
|
||||
аннотирован `.Produces<T>()`, так что схема полностью описывает и тела запросов, и тела ответов.
|
||||
Ниже — полный контракт, сверенный построчно с кодом (`backend/src/PnvPanel.Api/Endpoints/*.cs`).
|
||||
|
||||
## Auth
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ----- | --------------------------- | ------ | ---------------------------------------------------- |
|
||||
| POST | `/api/auth/register` | — | Регистрация `{ username, password }` |
|
||||
| POST | `/api/auth/login` | — | Вход `{ username, password }` → access (body) + refresh (httpOnly cookie) |
|
||||
| POST | `/api/auth/refresh` | — | Обновление access по refresh-cookie (ротация) |
|
||||
| POST | `/api/auth/logout` | user | Отзыв refresh-токена |
|
||||
| POST | `/api/auth/change-password` | user | Смена пароля `{ currentPassword, newPassword }` |
|
||||
| GET | `/api/auth/me` | user | Текущий профиль + роль + `isActivated` + `telegramLinked` |
|
||||
| DELETE| `/api/auth/me` | user | Самоудаление аккаунта (отзыв всех конфигов + удаление данных; аудит анонимизируется) |
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ----- | --------------------------- | ------ | ------------------------------------------ | -------------------------------------- |
|
||||
| POST | `/api/auth/register` | — | `{ userName, password }` | `{ id, userName }` |
|
||||
| POST | `/api/auth/login` | — | `{ userName, password }` | `{ accessToken, expiresAt, user }` + refresh в httpOnly cookie |
|
||||
| POST | `/api/auth/refresh` | — | — (refresh из cookie) | то же, что login; ротация cookie |
|
||||
| POST | `/api/auth/logout` | user | — | `204 No Content` |
|
||||
| POST | `/api/auth/change-password` | user | `{ currentPassword, newPassword }` | `204 No Content` |
|
||||
| GET | `/api/auth/me` | user | — | `{ id, userName, role, isActivated, telegramLinked }` |
|
||||
| DELETE| `/api/auth/me` | user | — | `204 No Content` |
|
||||
|
||||
> **Вход по username.** Email в системе не используется. Забыт пароль:
|
||||
> при привязанном Telegram — восстановление через бота; иначе — сброс админом (см. Admin).
|
||||
`user`/ответ `/me` — **`role` строкой** (одна роль, не массив). Группа `/api/auth/*` под общим
|
||||
rate-limit'ом (`RateLimiting:AuthPermitLimit`, по умолчанию 20 запросов/мин).
|
||||
|
||||
> **Вход по username.** Email в системе не используется. Забыт пароль: при привязанном
|
||||
> Telegram — вход без пароля через бота и смена пароля в настройках; иначе — сброс админом (см. Admin).
|
||||
|
||||
## Auth — Telegram (привязка и passwordless-вход)
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ----- | --------------------------------------------- | ---- | -------------------------------------------------------------- |
|
||||
| POST | `/api/auth/telegram/link-token` | user | Создать токен привязки → `{ deepLink, qr, expiresAt }` |
|
||||
| POST | `/api/auth/telegram/unlink` | user | Отвязать Telegram от аккаунта |
|
||||
| POST | `/api/auth/telegram/login-request` | — | Инициировать вход → `{ requestId, deepLink, qr, expiresAt }` |
|
||||
| GET | `/api/auth/telegram/login-request/{id}` | — | Статус запроса; при `Approved` выдаёт access + refresh-cookie |
|
||||
Группа `/api/auth/telegram/*`, тот же rate-limit, что и `/api/auth/*`.
|
||||
|
||||
`GET …/login-request/{id}` (поллинг; альтернатива — событие SignalR) → варианты ответа:
|
||||
| Метод | Путь | Роль | Тело ответа |
|
||||
| ----- | --------------------------------------------- | ---- | ------------------------------------------------------ |
|
||||
| POST | `/api/auth/telegram/link-token` | user | `{ deepLink, expiresAt }` |
|
||||
| POST | `/api/auth/telegram/unlink` | user | `204 No Content` |
|
||||
| POST | `/api/auth/telegram/login-request` | — | `{ requestId, deepLink, expiresAt }` |
|
||||
| GET | `/api/auth/telegram/login-request/{id}` | — | см. ниже |
|
||||
|
||||
`deepLink` — `null`, если `Telegram:BotUsername` не настроен (бот не привязан к инстансу), иначе
|
||||
`https://t.me/<bot>?start=link_<token>` / `?start=login_<requestId>`. **QR backend не рендерит** —
|
||||
фронт строит QR из `deepLink` сам (`qrcode.react`).
|
||||
|
||||
`GET …/login-request/{id}` (поллинг) → варианты ответа:
|
||||
```json
|
||||
// ожидание
|
||||
{ "status": "Pending" }
|
||||
// подтверждено — выпуск токенов (refresh уходит в httpOnly cookie), запрос → Consumed
|
||||
{ "status": "Approved", "accessToken": "…", "expiresAt": "…", "user": { "id": "…", "roles": ["User"] } }
|
||||
// отклонено / истекло
|
||||
{ "status": "Rejected" } // | "Expired"
|
||||
// ожидание / отклонено / истекло — accessToken/expiresAt/user всегда null, кроме Approved
|
||||
{ "status": "Pending", "accessToken": null, "expiresAt": null, "user": null }
|
||||
// подтверждено — выпуск токенов (refresh уходит в httpOnly cookie), запрос помечается Consumed
|
||||
{ "status": "Approved", "accessToken": "…", "expiresAt": "…", "user": { "id": "…", "userName": "…", "role": "user", "isActivated": true, "telegramLinked": true } }
|
||||
```
|
||||
`status` — одно из `Pending`/`Approved`/`Rejected`/`Expired`/`Consumed`.
|
||||
|
||||
> Сами апдейты Telegram (`/start`, кнопки) обрабатывает in-process бот (long polling), а не HTTP-эндпоинты.
|
||||
> Контракты команд бота — в [telegram-bot.md](telegram-bot.md).
|
||||
> Апдейты Telegram (`/start`, кнопки, `/configs`) обрабатывает in-process бот (long polling), а не
|
||||
> HTTP-эндпоинты. Команды бота — в [telegram-bot.md](telegram-bot.md).
|
||||
|
||||
## Configs (пользователь)
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ------ | --------------------------------- | ---- | ------------------------------------------------- |
|
||||
| GET | `/api/inbounds/available` | user | Инбаунды, доступные роли пользователя (для выбора при создании) |
|
||||
| GET | `/api/configs` | user | Список своих конфигов (пагинация) |
|
||||
| POST | `/api/configs` | user | Создать конфиг `{ inboundId, label?, deviceLimit? }` (проверки: активирован, квота роли, доступ роли к инбаунду) |
|
||||
| PATCH | `/api/configs/{id}` | user | Изменить `{ label?, deviceLimit? }` (deviceLimit → `limitIp` в 3x-ui) |
|
||||
| GET | `/api/configs/{id}` | user | Детали конфига (метка, трафик, устройства, статус) |
|
||||
| GET | `/api/configs/{id}/link` | user | Connection string + subscriptionUrl + QR-payload |
|
||||
| POST | `/api/configs/{id}/rotate` | user | Перевыпустить конфиг (новый UUID/ссылка; квоту не тратит) |
|
||||
| DELETE | `/api/configs/{id}` | user | Отозвать конфиг (удаляет клиента в 3x-ui) |
|
||||
| GET | `/api/subscription` | user | URL агрегированной подписки пользователя (все активные конфиги) |
|
||||
Группа `/api` (не вложена дальше), `RequireAuthorization()`.
|
||||
|
||||
`POST /api/configs` → `201 Created`:
|
||||
```json
|
||||
{
|
||||
"id": "…", "protocol": "Vless", "location": "DE",
|
||||
"link": "vless://…", "subscriptionUrl": "https://…/sub/…",
|
||||
"trafficLimitBytes": 53687091200, "expiresAt": "2026-08-01T00:00:00Z",
|
||||
"status": "Active"
|
||||
}
|
||||
```
|
||||
| Метод | Путь | Тело запроса | Тело ответа |
|
||||
| ------ | --------------------------------- | ------------------------------------ | --------------------------------------- |
|
||||
| GET | `/api/inbounds/available` | — | `AvailableInboundDto[]` |
|
||||
| GET | `/api/configs` | — | `{ configs: VpnConfigDto[], maxConfigs }` — **без пагинации**, весь список сразу |
|
||||
| POST | `/api/configs` | `{ inboundId, label?, deviceLimit? }`| `VpnConfigDto` (`200 OK`, не 201) |
|
||||
| PATCH | `/api/configs/{id}` | `{ label?, deviceLimit? }` | `VpnConfigDto` |
|
||||
| POST | `/api/configs/{id}/rotate` | — | `VpnConfigDto` (новый `id` тот же, новый `SubscriptionToken`) |
|
||||
| DELETE | `/api/configs/{id}` | — | `204 No Content` |
|
||||
| GET | `/api/configs/{id}/link` | — | `{ connectionString, subscriptionUrl }` |
|
||||
| GET | `/api/subscription` | — | `{ subscriptionUrl }` |
|
||||
|
||||
**Нет отдельного `GET /api/configs/{id}`** — детали конфига берутся из списка `GET /api/configs`.
|
||||
`VpnConfigDto`: `{ id, label, protocol, location, deviceLimit, usedUpBytes, usedDownBytes, expiresAt,
|
||||
status, createdAt }`. `expiresAt` в MVP всегда `null` (лимиты по сроку не реализованы — см.
|
||||
[domain-model.md](domain-model.md)). Ссылка подключения **не приходит вместе с созданием** — фронт
|
||||
запрашивает `GET .../link` отдельно, по кнопке на карточке конфига; QR строится на фронте из
|
||||
`connectionString`.
|
||||
|
||||
`POST /api/configs` без активации → `403` (`Configs.NotActivated`); сверх квоты роли → `409`
|
||||
(`Configs.QuotaExceeded`).
|
||||
|
||||
## Apps — каталог приложений
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ------ | -------------------------- | ----- | ---------------------------------------------------- |
|
||||
| GET | `/api/apps` | user | Включённые приложения, **сгруппированы по ОС** (для страницы инструкций) |
|
||||
| GET | `/api/admin/apps` | admin | Все приложения (вкл. выключенные) |
|
||||
| POST | `/api/admin/apps` | admin | Добавить `{ name, downloadUrl, operatingSystem, description?, iconUrl?, sortOrder? }` |
|
||||
| PUT | `/api/admin/apps/{id}` | admin | Изменить приложение (в т.ч. `isEnabled`) |
|
||||
| DELETE | `/api/admin/apps/{id}` | admin | Удалить приложение |
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ------ | -------------------------- | ----- | --------------------------------------------------------------------------------- | ------------- |
|
||||
| GET | `/api/apps` | user | — | `Record<OsPlatform, ClientAppDto[]>` |
|
||||
| GET | `/api/admin/apps` | admin | — | `AdminAppDto[]` (вкл. выключенные) |
|
||||
| POST | `/api/admin/apps` | admin | `{ name, downloadUrl, operatingSystem, description?, iconUrl?, sortOrder }` | `AdminAppDto` |
|
||||
| PUT | `/api/admin/apps/{id}` | admin | `{ name, downloadUrl, operatingSystem, description?, iconUrl?, sortOrder, isEnabled }` | `AdminAppDto` |
|
||||
| DELETE | `/api/admin/apps/{id}` | admin | — | `204 No Content` |
|
||||
|
||||
`GET /api/apps` → пример:
|
||||
`GET /api/apps` → пример (только `isEnabled == true`, ОС без приложений в ответе отсутствует):
|
||||
```json
|
||||
{
|
||||
"Android": [ { "id": "…", "name": "v2rayNG", "downloadUrl": "https://…", "iconUrl": null } ],
|
||||
"iOS": [ { "id": "…", "name": "Hiddify", "downloadUrl": "https://…", "iconUrl": null } ]
|
||||
"Android": [ { "id": "…", "name": "v2rayNG", "downloadUrl": "https://…", "description": null, "iconUrl": null } ],
|
||||
"IOS": [ { "id": "…", "name": "Hiddify", "downloadUrl": "https://…", "description": null, "iconUrl": null } ]
|
||||
}
|
||||
```
|
||||
Значение `OsPlatform` в C#/JSON — `IOS` (не `iOS`).
|
||||
|
||||
## Activation (пользователь)
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ----- | --------------------------- | ---- | ------------------------------------------------------ |
|
||||
| GET | `/api/activation/status` | user | Статус активации + текущий `Pending`-запрос (если есть)|
|
||||
| POST | `/api/activation/request` | user | Запросить активацию `{ comment? }` (напр. «я Никита») |
|
||||
|
||||
`GET /api/configs` для неактивированного пользователя вернёт пустой список; `POST /api/configs`
|
||||
до активации → `403` (или `409` с кодом `NotActivated`).
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ----- | --------------------------- | ---- | ----------------- | -------------------------------------------------------- |
|
||||
| GET | `/api/activation/status` | user | — | `{ isActivated, pendingRequest: { id, comment, createdAt } \| null }` |
|
||||
| POST | `/api/activation/request` | user | `{ comment? }` | `{ id, comment, createdAt }` |
|
||||
|
||||
## Admin — Activation, Roles
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ------ | ----------------------------------------------- | ----- | --------------------------------------------------- |
|
||||
| GET | `/api/admin/activation-requests` | admin | Список запросов активации (фильтр по статусу) |
|
||||
| POST | `/api/admin/activation-requests/{id}/approve` | admin | Одобрить → пользователь активирован |
|
||||
| POST | `/api/admin/activation-requests/{id}/reject` | admin | Отклонить `{ reason? }` |
|
||||
| GET | `/api/admin/roles` | admin | Список ролей с квотами |
|
||||
| POST | `/api/admin/roles` | admin | Создать роль `{ name, maxConfigs }` |
|
||||
| PUT | `/api/admin/roles/{id}` | admin | Изменить роль (напр. `maxConfigs`) |
|
||||
| DELETE | `/api/admin/roles/{id}` | admin | Удалить роль (нельзя системные `admin`/`user`) |
|
||||
| PATCH | `/api/admin/users/{id}/role` | admin | Сменить роль пользователю `{ roleId }` (ровно одна) |
|
||||
| PATCH | `/api/admin/users/{id}/activation` | admin | Активировать/деактивировать напрямую `{ isActivated }`|
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ------ | ----------------------------------------------- | ----- | ----------------------- | ------------- |
|
||||
| GET | `/api/admin/activation-requests` | admin | query: `statusFilter?, page=1, pageSize=20` | `PagedList<ActivationRequestAdminDto>` |
|
||||
| POST | `/api/admin/activation-requests/{id}/approve` | admin | — | `204 No Content` |
|
||||
| POST | `/api/admin/activation-requests/{id}/reject` | admin | `{ reason? }` | `204 No Content` |
|
||||
| GET | `/api/admin/roles` | admin | — | `RoleDto[]` |
|
||||
| POST | `/api/admin/roles` | admin | `{ name, maxConfigs }` | `RoleDto` |
|
||||
| PUT | `/api/admin/roles/{id}` | admin | `{ maxConfigs }` | `RoleDto` |
|
||||
| DELETE | `/api/admin/roles/{id}` | admin | — | `204 No Content` (системные `admin`/`user` удалить нельзя) |
|
||||
| PATCH | `/api/admin/users/{id}/role` | admin | `{ roleId }` | `204 No Content` |
|
||||
|
||||
Нет отдельного эндпоинта «активировать напрямую без запроса» — активация только через
|
||||
approve/reject над `ActivationRequest`.
|
||||
|
||||
## Admin — Nodes
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ------ | ----------------------------- | ----- | ----------------------------------------- |
|
||||
| GET | `/api/admin/nodes` | admin | Список нод + статусы |
|
||||
| POST | `/api/admin/nodes` | admin | Подключить ноду `{ name, baseAddress, username, password, location }` |
|
||||
| PUT | `/api/admin/nodes/{id}` | admin | Изменить ноду (в т.ч. `isEnabled`) |
|
||||
| DELETE | `/api/admin/nodes/{id}` | admin | Удалить ноду |
|
||||
| POST | `/api/admin/nodes/{id}/sync` | admin | Пересинхронизировать inbounds с 3x-ui |
|
||||
| POST | `/api/admin/nodes/{id}/probe` | admin | Проверить доступность |
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ------ | ----------------------------- | ----- | ---------------------------------------------------------------------------- | ------------- |
|
||||
| GET | `/api/admin/nodes` | admin | — | `NodeDto[]` |
|
||||
| POST | `/api/admin/nodes` | admin | `{ name, baseAddress, username, password, location? }` | `NodeDto` |
|
||||
| PUT | `/api/admin/nodes/{id}` | admin | `{ name, location?, isEnabled, username?, password? }` | `NodeDto` |
|
||||
| DELETE | `/api/admin/nodes/{id}` | admin | — | `204 No Content` |
|
||||
| POST | `/api/admin/nodes/{id}/sync` | admin | — | `{ inboundsSynced, status }` |
|
||||
| POST | `/api/admin/nodes/{id}/probe` | admin | — | `{ isReachable, errorMessage, status }` |
|
||||
|
||||
`DELETE /api/admin/nodes/{id}` удаляет её инбаунды каскадно **без проверки существующих конфигов**
|
||||
на них — известный пробел (см. [tech-stack.md](tech-stack.md)), а не осознанная защита.
|
||||
`username`/`password` в `PUT` — оба опциональны; креденшлы меняются, только если заданы **оба**.
|
||||
|
||||
## Admin — Inbounds
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ----- | -------------------------------------- | ----- | ---------------------------------------- |
|
||||
| GET | `/api/admin/inbounds` | admin | Список inbounds (по нодам) + `allowedRoleIds`, `displayName` |
|
||||
| PUT | `/api/admin/inbounds/{id}/publish` | admin | Опубликовать/снять `{ isPublished, displayName?, allowedRoleIds[], maxClients? }` |
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ----- | -------------------------------------- | ----- | ---------------------------------------------------------------------------- | ------------- |
|
||||
| GET | `/api/admin/inbounds` | admin | query: `nodeId?` | `InboundDto[]` |
|
||||
| PUT | `/api/admin/inbounds/{id}/publish` | admin | `{ isPublished, displayName?, allowedRoleIds?, maxClients? }` | `InboundDto` |
|
||||
|
||||
## Admin — Users & Stats
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| ----- | --------------------------------- | ----- | ----------------------------------------- |
|
||||
| GET | `/api/admin/users` | admin | Пользователи (пагинация, поиск) |
|
||||
| PATCH | `/api/admin/users/{id}/block` | admin | Блокировать/разблокировать `{ isBlocked }` (при блоке — отключить конфиги в 3x-ui) |
|
||||
| POST | `/api/admin/users/{id}/reset-password` | admin | Сбросить пароль пользователю без привязки Telegram (выдать временный/задать новый) |
|
||||
| GET | `/api/admin/users/{id}/configs` | admin | Конфиги пользователя |
|
||||
| DELETE| `/api/admin/configs/{id}` | admin | Принудительно отозвать любой конфиг |
|
||||
| GET | `/api/admin/stats` | admin | Сводная статистика (пользователи, конфиги, трафик) |
|
||||
| GET | `/api/admin/audit` | admin | Журнал действий (`AuditLog`, пагинация, фильтры) |
|
||||
| Метод | Путь | Роль | Тело запроса | Тело ответа |
|
||||
| ------ | ---------------------------------------- | ----- | --------------------------- | ------------- |
|
||||
| GET | `/api/admin/users` | admin | query: `page, pageSize, search?` | `PagedList<UserSummaryDto>` |
|
||||
| PATCH | `/api/admin/users/{id}/block` | admin | — | `204 No Content` |
|
||||
| PATCH | `/api/admin/users/{id}/unblock` | admin | — | `204 No Content` |
|
||||
| POST | `/api/admin/users/{id}/reset-password` | admin | `{ newPassword }` | `204 No Content` |
|
||||
| GET | `/api/admin/users/{id}/configs` | admin | — | `VpnConfigDto[]` |
|
||||
| DELETE | `/api/admin/configs/{id}` | admin | — | `204 No Content` (принудительный отзыв любого конфига) |
|
||||
| GET | `/api/admin/stats` | admin | — | `StatsDto` |
|
||||
| GET | `/api/admin/audit` | admin | query: `page, pageSize` | `PagedList<AuditLogDto>` |
|
||||
|
||||
**Блокировка/разблокировка — два отдельных эндпоинта без тела**, не один переключатель `isBlocked`.
|
||||
`StatsDto`: `{ totalUsers, activatedUsers, pendingActivationRequests, totalNodes, onlineNodes,
|
||||
totalConfigs, activeConfigs, totalUsedUpBytes, totalUsedDownBytes }` — считается на лету при запросе,
|
||||
не кэшируется.
|
||||
|
||||
## Public — Subscription
|
||||
|
||||
| Метод | Путь | Роль | Описание |
|
||||
| Метод | Путь | Роль | Ответ |
|
||||
| ----- | ----------------- | ---- | ---------------------------------------------------------------- |
|
||||
| GET | `/sub/{token}` | — | Подписка (base64-список ссылок). Токен — либо `AppUser.SubscriptionToken` (**все активные конфиги юзера**), либо `VpnConfig.SubscriptionToken` (**один конфиг**). Без `/api`. |
|
||||
| GET | `/sub/{token}` | — | `text/plain`, base64 от списка connection strings, `\n`-разделены |
|
||||
|
||||
Rate-limited; отключённые/отозванные конфиги в выдачу не попадают; неизвестный/погашенный токен → 404.
|
||||
Ответ отдаёт заголовок **`Subscription-Userinfo`** (`upload`/`download`/`total`/`expire`) — клиенты
|
||||
(v2rayN/Nekoray и т.п.) показывают остаток трафика/срок. Также `profile-update-interval`.
|
||||
Вне `/api` (публичный эндпоинт для VPN-клиентов), под тем же rate-limit'ом, что и `/api/auth/*`.
|
||||
Токен — либо `AppUser.SubscriptionToken` (**все активные конфиги юзера**), либо
|
||||
`VpnConfig.SubscriptionToken` (**один конфиг**); пробуются по очереди, первый успешный — в ответе.
|
||||
Неизвестный/погашенный токен → `404`. Заголовки ответа:
|
||||
`Subscription-Userinfo: upload=<up>; download=<down>; total=<up+down>; expire=<unix|0>` и
|
||||
`Profile-Update-Interval: 12` — их читают клиенты (v2rayN/Nekoray и т.п.), чтобы показать остаток.
|
||||
|
||||
## SignalR — Hub `/hubs/panel`
|
||||
|
||||
Авторизация — тем же JWT (query `access_token` или заголовок). Группы: `user:{userId}`, `admins`.
|
||||
Авторизация — тем же JWT. Группы: `user:{userId}` (личные события), `admins` (админам).
|
||||
|
||||
### Server → Client
|
||||
|
||||
| Событие | Payload | Кому |
|
||||
| ---------------------- | ------------------------------------------------------------- | ------------ |
|
||||
| `configTrafficUpdated` | `{ configId, usedUpBytes, usedDownBytes, limitBytes }` | владельцу |
|
||||
| `configTrafficUpdated` | `{ configId, usedUpBytes, usedDownBytes }` | владельцу |
|
||||
| `configStatusChanged` | `{ configId, status }` | владельцу |
|
||||
| `nodeStatusChanged` | `{ nodeId, status, lastSyncAt }` | `admins` |
|
||||
| `activationRequested` | `{ requestId, userId, username, comment, createdAt }` | `admins` |
|
||||
| `userActivated` | `{ userId }` | владельцу |
|
||||
| `nodeStatusChanged` | `{ nodeId, status, lastSyncAt }` | `admins` |
|
||||
| `activationRequested` | `{ requestId, userId, userName, comment, createdAt }` | `admins` |
|
||||
| `userActivated` | `{ userId }` | владельцу |
|
||||
|
||||
### Client → Server
|
||||
MVP — клиент только слушает (группировка по пользователю на сервере при подключении по `UserId` из JWT).
|
||||
Клиент только слушает; группировка по пользователю происходит на сервере при подключении, по
|
||||
`UserId` из JWT (плюс `admins`, если роль админская).
|
||||
|
||||
## Коды ошибок
|
||||
|
||||
| Код | Когда |
|
||||
| --- | -------------------------------------------------- |
|
||||
| 400 | Ошибка валидации (`errors` в ProblemDetails) |
|
||||
| 401 | Нет/просрочен токен |
|
||||
| 403 | Нет прав (роль/владение/не активирован/роль без доступа к инбаунду) |
|
||||
| 404 | Ресурс не найден |
|
||||
| 409 | Конфликт домена (превышена квота роли, дубликат, уже есть Pending-запрос активации) |
|
||||
| 422 | Нарушение инварианта домена |
|
||||
| 429 | Rate limit |
|
||||
| 502 | Ошибка/недоступность ноды 3x-ui (при необходимости)|
|
||||
| Код | Когда |
|
||||
| --- | -------------------------------------------------------------------- |
|
||||
| 400 | Ошибка валидации (FluentValidation, не на все команды — см. [backend-conventions.md](backend-conventions.md)) |
|
||||
| 401 | Нет/просрочен/невалиден access-токен |
|
||||
| 403 | Нет прав по роли, либо `Configs.NotActivated` |
|
||||
| 404 | Ресурс не найден |
|
||||
| 409 | Конфликт домена: `Configs.QuotaExceeded`, дубликат имени пользователя при регистрации, уже есть `Pending`-запрос активации |
|
||||
| 422 | Прочие управляемые ошибки, не подошедшие под коды выше |
|
||||
| 429 | Rate limit (`/api/auth/*`, `/api/auth/telegram/*`, `/sub/{token}`) |
|
||||
| 500 | Необработанное исключение (перехватывается `UseExceptionHandler()`, тело без деталей) |
|
||||
|
||||
`502`/недоступность 3x-ui наружу не пробрасывается — ошибка гейтвея становится `Result.Failure` и
|
||||
маппится в один из кодов выше (обычно 422), либо конфиг остаётся в старом статусе, если это фоновая синхронизация.
|
||||
|
||||
Reference in New Issue
Block a user