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"