- /admin/users: list with search, role filter and cursor pages; create with a one-time temporary password; rename, change role, block; reset password; delete - every change is checked against the role policy, a forbidden field rejects the whole patch; the last active admin is locked in the transaction and cannot be demoted, blocked or deleted - role change and blocking revoke access tokens at once; blocking and password reset also end all sessions - /admin/audit: newest-first journal with actor names - path and query parameter errors are problem+json now Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
759 lines
23 KiB
YAML
759 lines
23 KiB
YAML
openapi: 3.0.3
|
||
info:
|
||
title: Pipu Music API
|
||
version: 0.1.0
|
||
description: |
|
||
Публичный REST API Pipu Music. Источник истины для сервера (oapi-codegen)
|
||
и клиентов (openapi-typescript). Соглашения — docs/api.md.
|
||
servers:
|
||
- url: /
|
||
tags:
|
||
- name: system
|
||
- name: auth
|
||
- name: me
|
||
- name: admin
|
||
security:
|
||
- bearerAuth: []
|
||
paths:
|
||
/healthz:
|
||
get:
|
||
operationId: getHealthz
|
||
tags: [system]
|
||
security: []
|
||
summary: Живость процесса
|
||
responses:
|
||
"200":
|
||
description: Процесс жив
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/HealthStatus"
|
||
/readyz:
|
||
get:
|
||
operationId: getReadyz
|
||
tags: [system]
|
||
security: []
|
||
summary: Готовность обслуживать запросы (зависимости доступны)
|
||
responses:
|
||
"200":
|
||
description: Все зависимости доступны
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/ReadyStatus"
|
||
"503":
|
||
description: Одна или несколько зависимостей недоступны
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/ReadyStatus"
|
||
/api/v1/server-info:
|
||
get:
|
||
operationId: getServerInfo
|
||
tags: [system]
|
||
security: []
|
||
summary: Сведения о сервере для клиентов
|
||
description: |
|
||
Вызывается без аутентификации: веб и Android проверяют версию сервера,
|
||
совместимость клиента и поддерживаемые языки (экран подключения).
|
||
responses:
|
||
"200":
|
||
description: Сведения о сервере
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/ServerInfo"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/auth/login:
|
||
post:
|
||
operationId: login
|
||
tags: [auth]
|
||
security: []
|
||
summary: Вход по логину и паролю
|
||
description: |
|
||
Веб (`client_type: web`) получает refresh-токен в cookie `pipu_rt`
|
||
(`HttpOnly; Secure; SameSite=Strict; Path=/api/v1/auth`), Android — в поле `refresh_token`.
|
||
Неверный логин или пароль, а также временно заблокированный перебором вход — всегда
|
||
`401 auth.invalid_credentials`. Превышен лимит попыток с адреса — `429 auth.too_many_attempts`.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/LoginRequest"
|
||
responses:
|
||
"200":
|
||
description: Успешный вход
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AuthTokens"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/auth/refresh:
|
||
post:
|
||
operationId: refreshTokens
|
||
tags: [auth]
|
||
security: []
|
||
summary: Ротация refresh-токена
|
||
description: |
|
||
Refresh-токен берётся из cookie `pipu_rt` (веб) или из тела (Android). Старый токен
|
||
становится использованным; его повторное предъявление отзывает всю семью токенов.
|
||
parameters:
|
||
- name: pipu_rt
|
||
in: cookie
|
||
required: false
|
||
schema:
|
||
type: string
|
||
requestBody:
|
||
required: false
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/RefreshRequest"
|
||
responses:
|
||
"200":
|
||
description: Новая пара токенов
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AuthTokens"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/auth/logout:
|
||
post:
|
||
operationId: logout
|
||
tags: [auth]
|
||
summary: Выход — отзыв текущей сессии
|
||
responses:
|
||
"204":
|
||
description: Сессия завершена, cookie `pipu_rt` очищена
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/auth/logout-all:
|
||
post:
|
||
operationId: logoutAll
|
||
tags: [auth]
|
||
summary: Выход на всех устройствах
|
||
responses:
|
||
"204":
|
||
description: Все сессии пользователя завершены
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/me:
|
||
get:
|
||
operationId: getMe
|
||
tags: [me]
|
||
summary: Текущий пользователь
|
||
responses:
|
||
"200":
|
||
description: Профиль
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Me"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
patch:
|
||
operationId: updateMe
|
||
tags: [me]
|
||
summary: Изменить имя, язык, настройки интерфейса
|
||
description: Поля `ui_prefs` сливаются с сохранёнными; не переданные поля не меняются.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/UpdateMeRequest"
|
||
responses:
|
||
"200":
|
||
description: Обновлённый профиль
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/Me"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/me/password:
|
||
post:
|
||
operationId: changePassword
|
||
tags: [me]
|
||
summary: Сменить пароль
|
||
description: |
|
||
`current_password` обязателен, кроме случая `must_change_password` (вход по временному паролю).
|
||
Все сессии отзываются; текущему устройству выдаётся новая пара токенов.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/ChangePasswordRequest"
|
||
responses:
|
||
"200":
|
||
description: Пароль изменён, новая пара токенов
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AuthTokens"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/me/bootstrap-credentials:
|
||
post:
|
||
operationId: setBootstrapCredentials
|
||
tags: [me]
|
||
summary: Первый запуск — новые логин и пароль администратора
|
||
description: |
|
||
Только при `must_change_credentials`. Пароль должен отличаться от текущего.
|
||
Все прочие сессии отзываются, выдаётся новая пара токенов.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/BootstrapCredentialsRequest"
|
||
responses:
|
||
"200":
|
||
description: Учётные данные заменены, новая пара токенов
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AuthTokens"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/me/sessions:
|
||
get:
|
||
operationId: listMySessions
|
||
tags: [me]
|
||
summary: Активные сессии (устройства)
|
||
responses:
|
||
"200":
|
||
description: Список сессий, новые сверху
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/SessionList"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/me/sessions/{id}:
|
||
delete:
|
||
operationId: revokeMySession
|
||
tags: [me]
|
||
summary: Завершить сессию на устройстве
|
||
parameters:
|
||
- name: id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
responses:
|
||
"204":
|
||
description: Сессия завершена
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/admin/users:
|
||
get:
|
||
operationId: listUsers
|
||
tags: [admin]
|
||
summary: Пользователи (admin, moderator)
|
||
description: Сортировка по логину. `q` ищет по логину и имени без учёта регистра.
|
||
parameters:
|
||
- name: q
|
||
in: query
|
||
schema:
|
||
type: string
|
||
maxLength: 64
|
||
- name: role
|
||
in: query
|
||
schema:
|
||
$ref: "#/components/schemas/Role"
|
||
- $ref: "#/components/parameters/Limit"
|
||
- $ref: "#/components/parameters/Cursor"
|
||
responses:
|
||
"200":
|
||
description: Страница пользователей
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AdminUserPage"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
post:
|
||
operationId: createUser
|
||
tags: [admin]
|
||
summary: Создать пользователя
|
||
description: |
|
||
Пароль генерирует сервер и возвращает **один раз**; при первом входе его нужно сменить.
|
||
Модератор создаёт только `user`, админ — любые роли.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/CreateUserRequest"
|
||
responses:
|
||
"201":
|
||
description: Пользователь создан
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/CreatedUser"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/admin/users/{id}:
|
||
parameters:
|
||
- $ref: "#/components/parameters/UserId"
|
||
patch:
|
||
operationId: updateUser
|
||
tags: [admin]
|
||
summary: Изменить имя, роль, блокировку
|
||
description: |
|
||
Роль меняет только админ. Смена роли и блокировка мгновенно отзывают токены пользователя.
|
||
Последнего активного админа нельзя понизить или заблокировать — `409 users.last_admin`.
|
||
requestBody:
|
||
required: true
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/UpdateUserRequest"
|
||
responses:
|
||
"200":
|
||
description: Обновлённый пользователь
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AdminUser"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
delete:
|
||
operationId: deleteUser
|
||
tags: [admin]
|
||
summary: Удалить пользователя (admin)
|
||
responses:
|
||
"204":
|
||
description: Удалён
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/admin/users/{id}/reset-password:
|
||
parameters:
|
||
- $ref: "#/components/parameters/UserId"
|
||
post:
|
||
operationId: resetUserPassword
|
||
tags: [admin]
|
||
summary: Сбросить пароль
|
||
description: Новый временный пароль возвращается один раз; все сессии пользователя завершаются.
|
||
responses:
|
||
"200":
|
||
description: Временный пароль
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/TemporaryPassword"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
/api/v1/admin/audit:
|
||
get:
|
||
operationId: listAudit
|
||
tags: [admin]
|
||
summary: Журнал действий (admin), новые сверху
|
||
parameters:
|
||
- $ref: "#/components/parameters/Limit"
|
||
- $ref: "#/components/parameters/Cursor"
|
||
responses:
|
||
"200":
|
||
description: Страница журнала
|
||
content:
|
||
application/json:
|
||
schema:
|
||
$ref: "#/components/schemas/AuditPage"
|
||
default:
|
||
$ref: "#/components/responses/Problem"
|
||
components:
|
||
parameters:
|
||
Limit:
|
||
name: limit
|
||
in: query
|
||
schema:
|
||
type: integer
|
||
minimum: 1
|
||
maximum: 200
|
||
default: 50
|
||
Cursor:
|
||
name: cursor
|
||
in: query
|
||
description: Непрозрачный курсор из `next_cursor` предыдущей страницы
|
||
schema:
|
||
type: string
|
||
UserId:
|
||
name: id
|
||
in: path
|
||
required: true
|
||
schema:
|
||
type: string
|
||
format: uuid
|
||
securitySchemes:
|
||
bearerAuth:
|
||
type: http
|
||
scheme: bearer
|
||
bearerFormat: JWT
|
||
responses:
|
||
Problem:
|
||
description: Ошибка (RFC 9457)
|
||
content:
|
||
application/problem+json:
|
||
schema:
|
||
$ref: "#/components/schemas/Problem"
|
||
schemas:
|
||
HealthStatus:
|
||
type: object
|
||
required: [status]
|
||
properties:
|
||
status:
|
||
type: string
|
||
enum: [ok]
|
||
ReadyStatus:
|
||
type: object
|
||
required: [status, checks]
|
||
properties:
|
||
status:
|
||
type: string
|
||
enum: [ok, degraded]
|
||
checks:
|
||
type: object
|
||
description: Результат проверки каждой зависимости
|
||
additionalProperties:
|
||
type: string
|
||
description: "`ok` или `fail` (причина — только в логе сервиса)"
|
||
ServerInfo:
|
||
type: object
|
||
required: [name, version, api_version, supported_locales, min_android_version_code, bootstrap_required]
|
||
properties:
|
||
name:
|
||
type: string
|
||
example: pipu-music
|
||
version:
|
||
type: string
|
||
description: Версия сервера (SemVer)
|
||
example: 0.1.0
|
||
api_version:
|
||
type: string
|
||
example: v1
|
||
supported_locales:
|
||
type: array
|
||
items:
|
||
type: string
|
||
example: [ru, en]
|
||
min_android_version_code:
|
||
type: integer
|
||
description: Минимальный versionCode Android-приложения, которое поддерживает сервер
|
||
example: 10099
|
||
bootstrap_required:
|
||
type: boolean
|
||
description: Первый вход администратора ещё не завершён — клиент может показать подсказку
|
||
Role:
|
||
type: string
|
||
enum: [admin, moderator, user]
|
||
ClientType:
|
||
type: string
|
||
enum: [web, android]
|
||
LoginRequest:
|
||
type: object
|
||
required: [username, password, client_type]
|
||
properties:
|
||
username:
|
||
type: string
|
||
password:
|
||
type: string
|
||
client_type:
|
||
$ref: "#/components/schemas/ClientType"
|
||
device_name:
|
||
type: string
|
||
maxLength: 100
|
||
description: Например «Chrome / Windows», «Pixel 8»
|
||
RefreshRequest:
|
||
type: object
|
||
properties:
|
||
refresh_token:
|
||
type: string
|
||
description: Только Android; веб передаёт токен в cookie
|
||
AuthTokens:
|
||
type: object
|
||
required: [access_token, token_type, expires_in, user]
|
||
properties:
|
||
access_token:
|
||
type: string
|
||
token_type:
|
||
type: string
|
||
enum: [Bearer]
|
||
expires_in:
|
||
type: integer
|
||
description: Срок жизни access-токена, секунды
|
||
refresh_token:
|
||
type: string
|
||
description: "Только для `client_type: android`"
|
||
user:
|
||
$ref: "#/components/schemas/Me"
|
||
UiPrefs:
|
||
type: object
|
||
properties:
|
||
theme:
|
||
type: string
|
||
enum: [dark, light, system]
|
||
wave_variant:
|
||
type: string
|
||
enum: [orb, liquid]
|
||
web_player:
|
||
type: string
|
||
enum: [bar, island]
|
||
mobile_player:
|
||
type: string
|
||
enum: [cover, wave]
|
||
ambient:
|
||
type: boolean
|
||
crossfade_s:
|
||
type: integer
|
||
minimum: 0
|
||
maximum: 6
|
||
normalize:
|
||
type: boolean
|
||
Me:
|
||
type: object
|
||
required: [id, username, display_name, role, locale, ui_prefs, must_change_password, must_change_credentials, created_at]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
username:
|
||
type: string
|
||
display_name:
|
||
type: string
|
||
nullable: true
|
||
role:
|
||
$ref: "#/components/schemas/Role"
|
||
locale:
|
||
type: string
|
||
ui_prefs:
|
||
$ref: "#/components/schemas/UiPrefs"
|
||
must_change_password:
|
||
type: boolean
|
||
must_change_credentials:
|
||
type: boolean
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
UpdateMeRequest:
|
||
type: object
|
||
properties:
|
||
display_name:
|
||
type: string
|
||
maxLength: 64
|
||
description: Пустая строка — сбросить имя
|
||
locale:
|
||
type: string
|
||
ui_prefs:
|
||
$ref: "#/components/schemas/UiPrefs"
|
||
ChangePasswordRequest:
|
||
type: object
|
||
required: [new_password]
|
||
properties:
|
||
current_password:
|
||
type: string
|
||
new_password:
|
||
type: string
|
||
BootstrapCredentialsRequest:
|
||
type: object
|
||
required: [new_username, new_password]
|
||
properties:
|
||
new_username:
|
||
type: string
|
||
new_password:
|
||
type: string
|
||
AdminUser:
|
||
type: object
|
||
required: [id, username, display_name, role, locale, is_blocked, must_change_password, must_change_credentials, created_at, last_login_at]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
username:
|
||
type: string
|
||
display_name:
|
||
type: string
|
||
nullable: true
|
||
role:
|
||
$ref: "#/components/schemas/Role"
|
||
locale:
|
||
type: string
|
||
is_blocked:
|
||
type: boolean
|
||
must_change_password:
|
||
type: boolean
|
||
must_change_credentials:
|
||
type: boolean
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
last_login_at:
|
||
type: string
|
||
format: date-time
|
||
nullable: true
|
||
AdminUserPage:
|
||
type: object
|
||
required: [items, next_cursor]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: "#/components/schemas/AdminUser"
|
||
next_cursor:
|
||
type: string
|
||
nullable: true
|
||
CreateUserRequest:
|
||
type: object
|
||
required: [username, role]
|
||
properties:
|
||
username:
|
||
type: string
|
||
display_name:
|
||
type: string
|
||
maxLength: 64
|
||
role:
|
||
$ref: "#/components/schemas/Role"
|
||
CreatedUser:
|
||
type: object
|
||
required: [user, temporary_password]
|
||
properties:
|
||
user:
|
||
$ref: "#/components/schemas/AdminUser"
|
||
temporary_password:
|
||
type: string
|
||
description: Показывается один раз
|
||
UpdateUserRequest:
|
||
type: object
|
||
properties:
|
||
display_name:
|
||
type: string
|
||
maxLength: 64
|
||
description: Пустая строка — сбросить имя
|
||
role:
|
||
$ref: "#/components/schemas/Role"
|
||
is_blocked:
|
||
type: boolean
|
||
TemporaryPassword:
|
||
type: object
|
||
required: [temporary_password]
|
||
properties:
|
||
temporary_password:
|
||
type: string
|
||
AuditActor:
|
||
type: object
|
||
required: [id, username]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
username:
|
||
type: string
|
||
AuditEntry:
|
||
type: object
|
||
required: [id, actor, action, target_type, target_id, payload, created_at]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
actor:
|
||
description: null — система или удалённый пользователь
|
||
nullable: true
|
||
allOf:
|
||
- $ref: "#/components/schemas/AuditActor"
|
||
action:
|
||
type: string
|
||
example: user.create
|
||
target_type:
|
||
type: string
|
||
nullable: true
|
||
target_id:
|
||
type: string
|
||
format: uuid
|
||
nullable: true
|
||
payload:
|
||
type: object
|
||
nullable: true
|
||
additionalProperties: true
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
AuditPage:
|
||
type: object
|
||
required: [items, next_cursor]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: "#/components/schemas/AuditEntry"
|
||
next_cursor:
|
||
type: string
|
||
nullable: true
|
||
Session:
|
||
type: object
|
||
required: [id, device_name, client_type, created_at, last_used_at, current]
|
||
properties:
|
||
id:
|
||
type: string
|
||
format: uuid
|
||
device_name:
|
||
type: string
|
||
nullable: true
|
||
client_type:
|
||
$ref: "#/components/schemas/ClientType"
|
||
created_at:
|
||
type: string
|
||
format: date-time
|
||
description: Время входа
|
||
last_used_at:
|
||
type: string
|
||
format: date-time
|
||
current:
|
||
type: boolean
|
||
description: Сессия, из которой сделан запрос
|
||
SessionList:
|
||
type: object
|
||
required: [items]
|
||
properties:
|
||
items:
|
||
type: array
|
||
items:
|
||
$ref: "#/components/schemas/Session"
|
||
FieldError:
|
||
type: object
|
||
required: [field, code]
|
||
properties:
|
||
field:
|
||
type: string
|
||
code:
|
||
type: string
|
||
Problem:
|
||
type: object
|
||
required: [status, code]
|
||
properties:
|
||
type:
|
||
type: string
|
||
default: about:blank
|
||
status:
|
||
type: integer
|
||
code:
|
||
type: string
|
||
description: Машинный код ошибки для i18n, например `auth.invalid_credentials`
|
||
detail:
|
||
type: string
|
||
description: Пояснение для логов (не для показа пользователю)
|
||
params:
|
||
type: object
|
||
additionalProperties: true
|
||
errors:
|
||
type: array
|
||
description: Ошибки отдельных полей запроса
|
||
items:
|
||
$ref: "#/components/schemas/FieldError"
|