Files
PnvPanel/docs/api-design.md
T
Leonid Pershin b6637a1c03
CI / Backend (build + test) (push) Successful in 1m18s
CI / Frontend (lint + typecheck + build) (push) Successful in 32s
Add news feature with CRUD operations and real-time notifications
- Implemented news management functionality, allowing admins to create, read, update, and delete news posts.
- Introduced a new SignalR event for broadcasting news updates to all connected clients.
- Updated API documentation to include new endpoints for news management.
- Enhanced frontend with a dedicated news page and admin interface for managing news posts.
- Added necessary localization for news-related terms in both Russian and English.
2026-07-03 15:28:33 +03:00

20 KiB
Raw Blame History

API Design

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 (без версионирования). Схема генерируется нативным Microsoft.AspNetCore.OpenApi (/openapi/v1.json) и Scalar UI (/scalar) — каждый эндпоинт аннотирован .Produces<T>(), так что схема полностью описывает и тела запросов, и тела ответов. Ниже — полный контракт, сверенный построчно с кодом (backend/src/PnvPanel.Api/Endpoints/*.cs).

Auth

Метод Путь Роль Тело запроса Тело ответа
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
POST /api/auth/change-username user { newUserName } 204 No Content
GET /api/auth/me user { id, userName, role, isActivated, telegramLinked }
DELETE /api/auth/me user 204 No Content

user/ответ /merole строкой (одна роль, не массив). Группа /api/auth/* под общим rate-limit'ом (RateLimiting:AuthPermitLimit, по умолчанию 20 запросов/мин).

Вход по username. Email в системе не используется. Забыт пароль: при привязанном Telegram — вход без пароля через бота и смена пароля в настройках; иначе — сброс админом (см. Admin).

Auth — Telegram (привязка и passwordless-вход)

Группа /api/auth/telegram/*, тот же rate-limit, что и /api/auth/*.

Метод Путь Роль Тело ответа
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} см. ниже

deepLinknull, если Telegram:BotToken не настроен или Bot API недоступен (username бота панель получает сама через getMe, см. telegram-bot.md), иначе https://t.me/<bot>?start=link_<token> / ?start=login_<requestId>. QR backend не рендерит — фронт строит QR из deepLink сам (qrcode.react).

GET …/login-request/{id} (поллинг) → варианты ответа:

// ожидание / отклонено / истекло — 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, кнопки, /configs) обрабатывает in-process бот (long polling), а не HTTP-эндпоинты. Команды бота — в telegram-bot.md.

Configs (пользователь)

Группа /api (не вложена дальше), RequireAuthorization().

Метод Путь Тело запроса Тело ответа
GET /api/inbounds/available AvailableInboundDto[]
GET /api/configs { configs: VpnConfigDto[], maxConfigs }без пагинации, весь список сразу
POST /api/configs { inboundId, label? } VpnConfigDto (200 OK, не 201)
PATCH /api/configs/{id} { label? } 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, usedUpBytes, usedDownBytes, expiresAt, status, createdAt }. expiresAt всегда null (лимиты по сроку не реализованы — см. domain-model.md). Ссылка подключения не приходит вместе с созданием — фронт запрашивает GET .../link отдельно, по кнопке на карточке конфига; QR строится на фронте из connectionString.

POST /api/configs без активации → 403 (Configs.NotActivated); сверх квоты роли → 409 (Configs.QuotaExceeded).

Apps — каталог приложений

Метод Путь Роль Тело запроса Тело ответа
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 → пример (только isEnabled == true, ОС без приложений в ответе отсутствует):

{
  "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).

News — лента новостей

Метод Путь Роль Тело запроса Тело ответа
GET /api/news user query: page, pageSize PagedList<NewsPostDto>
GET /api/admin/news admin query: page, pageSize PagedList<NewsPostDto>
POST /api/admin/news admin { title, body } NewsPostDto
PUT /api/admin/news/{id} admin { title, body } NewsPostDto
DELETE /api/admin/news/{id} admin 204 No Content

Нет черновиков/отложенной публикации — POST сразу видна всем аутентифицированным пользователям и триггерит SignalR-событие newsPublished (см. ниже). NewsPostDto: { id, title, body, createdAt, updatedAt }.

Activation (пользователь)

Метод Путь Роль Тело запроса Тело ответа
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 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 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), а не осознанная защита. username/password в PUT — оба опциональны; креденшлы меняются, только если заданы оба.

Admin — Inbounds

Метод Путь Роль Тело запроса Тело ответа
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 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} text/plain, base64 от списка connection strings, \n-разделены

Вне /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. Группы: user:{userId} (личные события), admins (админам).

Server → Client

Событие Payload Кому
configTrafficUpdated { configId, usedUpBytes, usedDownBytes } владельцу
configStatusChanged { configId, status } владельцу
nodeStatusChanged { nodeId, status, lastSyncAt } admins
activationRequested { requestId, userId, userName, comment, createdAt } admins
userActivated { userId } владельцу
newsPublished { id, title, createdAt } все (broadcast)

Client → Server

Клиент только слушает; группировка по пользователю происходит на сервере при подключении, по UserId из JWT (плюс admins, если роль админская).

Коды ошибок

Код Когда
400 Ошибка валидации (FluentValidation, не на все команды — см. 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), либо конфиг остаётся в старом статусе, если это фоновая синхронизация.