- 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.
20 KiB
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/ответ /me — role строкой (одна роль, не массив). Группа /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} |
— | см. ниже |
deepLink — null, если 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), либо конфиг остаётся в старом статусе, если это фоновая синхронизация.