Files
pipu-music/api/openapi.yaml
T
mrleo1nidandClaude Opus 5.5 ed4a492dba
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
feat(api): sign-in, refresh rotation and bootstrap admin (M1)
- 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>
2026-10-20 06:12:25 +03:00

493 lines
15 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
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"