ci / contract / contract (сгенерированный код совпадает с openapi.yaml) (push) Successful in 1m15s
ci / api / api (push) Successful in 1m31s
ci / mobile / mobile (Metro-бандл Android) (push) Successful in 47s
ci / ts / ts (packages + web) (push) Successful in 39s
ci / docker / docker (образы собираются — их сервер собирает сам при обновлении) (push) Successful in 26s
- users, refresh_tokens and audit_log schema (migration 00002) - argon2id passwords with rehash on login, password policy, lockout with exponential back-off and a per-IP rate limit in Redis - Ed25519 JWT access tokens with kid-based key rotation; rotating refresh tokens with reuse detection (cookie for web, body for Android) - bootstrap admin on first start; must_change_credentials and must_change_password gate every other endpoint - /auth/login, /auth/refresh, /auth/logout(-all), /me, /me/password, /me/bootstrap-credentials, /me/sessions; server-info reports bootstrap_required - role policy from docs/auth-and-roles.md with table tests - problem+json errors with field errors; ru/en translations of the codes - install.sh and update.sh generate JWT_PRIVATE_KEY Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
493 lines
15 KiB
YAML
493 lines
15 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
|
||
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"
|
||
components:
|
||
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
|
||
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"
|