Files
pipu-music/api/openapi.yaml
T
mrleo1nidandClaude Opus 5.5 f075863f15
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 38s
ci / api / api (push) Successful in 18s
ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 35s
feat(api): user management and audit log for admins (M1)
- /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>
2026-10-20 06:18:02 +03:00

759 lines
23 KiB
YAML
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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"