Merge branch 'feature/H-3657' into 'master'

H-3657: добавил Personal Access Tokens в v2 swagger

See merge request hrbox-public/api!34
This commit was merged in pull request #34.
This commit is contained in:
2026-05-22 11:03:58 +00:00
+457 -3
View File
@@ -27,6 +27,86 @@ info:
{sessionId} - you will get this form /mobile/auth method
{sessionSecret} - you will get this form /mobile/auth method
the last one (x-hrbox-embed) is for opening web pages in WebView to remove website footer and header
---
# Авторизация по личному API-токену (Personal Access Token, PAT)
Для серверных интеграций (web-zaim и т.п.) удобнее не держать сессию,
а использовать постоянный токен. Токен работает **только на `/api/v2/*`**
(для `/api/v1/*`, `/api/v3/*`, `/admin/*` и веб-страниц — игнорируется).
## Получение токена
1. Юзеру, от имени которого будет работать интеграция, нужно право
`api-tokens-manage` (выдаётся точечно администратором).
2. Юзер открывает `/profile/settings/api-tokens` в веб-интерфейсе и жмёт
«Создать токен» — задаёт название и (опционально) дату истечения.
Plain-секрет **показывается один раз** в этом окне, после закрытия
восстановить нельзя.
3. Альтернативно — `POST /api/v2/user-api-token` под session-авторизацией
(см. ниже про management-эндпоинты). Plain также возвращается
один раз в поле `data.plain`.
Формат plain-токена: `hrb_` + 32 url-safe символов = 36 символов всего.
В БД хранится только HMAC-SHA-256 хеш и видимый префикс (первые 12
символов с `hrb_`).
## Использование токена
Подписывайте каждый запрос заголовком `Authorization`:
```
Authorization: Bearer hrb_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```
Tenant авторезолвится по `Host`-заголовку запроса, поэтому
`x-hrbox-tenant-id` / `x-hrbox-tenant-domain` **необязательны**.
Если всё же передаёте — будут строго провалидированы, невалидный
идентификатор тенанта → 400. `x-hrbox-session-id` / `x-hrbox-session-code`
при Bearer-аутентификации **не нужны**.
## Права и scopes
⚠️ **Гранулярных scopes у токена нет.** Токен наследует **все права того
юзера**, под которым выпущен. Это означает что для интеграции нужно
создать **отдельного системного пользователя** и выдать ему ровно те
права, которые требуются (например, `wallets-transactions` для нового
`/api/v2/admin/wallet*`). Не используйте свой личный аккаунт.
## Отзыв и жизненный цикл
- Юзер отзывает свой токен через `POST /api/v2/user-api-token/revoke`
(или из UI).
- Администратор с правом `api-tokens-admin` может посмотреть/отозвать
любой токен тенанта через `/api/v2/admin/user-api-token*`. Plain
админу **никогда не виден**, только префикс.
- При блокировке юзера (`reg_status_id = BLOCKED`) все его токены
автоматически отзываются. При разблокировке **обратно не восстанавливаются** —
нужно выпускать новые.
- Если у токена выставлен `expires_at` — после этой даты он перестаёт
работать сам (статус `EXPIRED`).
## Когда валидный токен всё равно даёт 401
`User::findIdentityByAccessToken` дополнительно вызывает
`User::isValidForDisplay()`, поэтому даже корректный live-токен
отдаёт `401 Login Required`, если у владельца:
- `is_hidden = true` (юзер скрыт администратором), либо
- `reg_status_id` **не входит** в `RegStatus::validForDisplay()`
(например `INVITED`, `BLOCKED` и пр.).
Для системного юзера-интегратора убедитесь, что он `is_hidden=false`
и в активном reg-статусе, иначе токен не пройдёт ни на одном
`/api/v2/*` эндпоинте.
## Управление токенами под Bearer запрещено
Management-эндпоинты (`/user-api-token*`, `/admin/user-api-token*`)
отдают `403`, если запрос аутентифицирован Bearer-токеном — нельзя
выпускать или отзывать токены от имени токена. Управляйте только
под обычной web-сессией.
contact:
email: yuriy@hrbox.io
@@ -67,6 +147,16 @@ tags:
description: Админский API транзакций кошелька — проведение начислений/списаний (form/excel) и просмотр.
- name: file-processing
description: Статусы асинхронных обработок файлов (excel-импорт/экспорт, архивы). Видны только записи текущего пользователя.
- name: user-api-token
description: |
Личные API-токены (Personal Access Tokens) для серверных интеграций.
Юзер управляет только своими токенами. Permission `api-tokens-manage`.
Management-эндпоинты доступны **только под обычной session-авторизацией**
(Bearer запрещён). См. секцию «Авторизация по личному API-токену» выше.
- name: admin-user-api-token
description: |
Админский обзор и отзыв любых PAT тенанта. Permission `api-tokens-admin`.
Plain-секрет недоступен — виден только `token_prefix`.
paths:
/mobile/bind/{id}/{token}:
@@ -7265,6 +7355,201 @@ paths:
schema:
$ref: '#/components/schemas/FileProcessing'
/user-api-token:
get:
tags: [user-api-token]
summary: Список своих API-токенов
description: |
Возвращает PAT текущего юзера. Plain-секрет уже недоступен —
для идентификации используется `token_prefix` (первые 12 символов,
начинаются с `hrb_`).
operationId: userApiTokenIndex
security:
- SessionAuth: []
parameters:
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: per-page
in: query
schema: { type: integer, minimum: 1, default: 20 }
- name: sort
in: query
schema:
type: string
enum: [created_at, "-created_at", last_used_at, "-last_used_at", expires_at, "-expires_at"]
description: По умолчанию `-created_at`.
responses:
200:
description: Список токенов
content:
application/json:
schema:
$ref: '#/components/schemas/UserApiTokenList'
403:
description: Запрос пришёл под Bearer-токеном (запрещено) или нет права `api-tokens-manage`.
/user-api-token/create:
post:
tags: [user-api-token]
summary: Создать новый API-токен (plain возвращается один раз)
description: |
Создаёт PAT для текущего юзера. **Plain-секрет (`data.plain`)
возвращается только в этом ответе и больше нигде не доступен** —
сохраните его сразу.
`expires_at` опционален; если задан, должен быть не в прошлом
(валидируется по началу текущего дня). Токен работает весь
указанный день включительно (23:59:59).
Доступно только под session-авторизацией (запрос под Bearer → 403).
operationId: userApiTokenCreate
security:
- SessionAuth: []
requestBody:
required: true
content:
application/json:
schema:
$ref: '#/components/schemas/UserApiTokenCreateForm'
responses:
200:
description: Токен создан (либо ошибки валидации в стандартном формате `httpJsonResponse`).
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/httpJsonResponse'
- type: object
properties:
data:
$ref: '#/components/schemas/UserApiTokenCreated'
403:
description: Запрос пришёл под Bearer-токеном или нет права `api-tokens-manage`.
/user-api-token/revoke:
post:
tags: [user-api-token]
summary: Отозвать свой API-токен
description: |
Идемпотентно: повторный revoke уже отозванного токена просто ничего не делает.
Чужой токен отозвать нельзя — будет 403/404.
Доступно только под session-авторизацией (запрос под Bearer → 403).
operationId: userApiTokenRevoke
security:
- SessionAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id]
properties:
id:
type: string
format: uuid
responses:
200:
description: Успех
content:
application/json:
schema:
$ref: '#/components/schemas/httpJsonResponse'
400:
description: Параметр `id` не строка или не uuid.
403:
description: Токен принадлежит другому юзеру, либо запрос под Bearer, либо нет права `api-tokens-manage`.
404:
description: Токен не найден.
/admin/user-api-token:
get:
tags: [admin-user-api-token]
summary: Список API-токенов всех юзеров тенанта
description: |
Админский обзор PAT. Permission `api-tokens-admin`. Plain-секрет
здесь **никогда не возвращается** (только `token_prefix`).
Доступно только под session-авторизацией (запрос под Bearer → 403).
operationId: adminUserApiTokenIndex
security:
- SessionAuth: []
parameters:
- name: page
in: query
schema: { type: integer, minimum: 1, default: 1 }
- name: per-page
in: query
schema: { type: integer, minimum: 1, default: 20 }
- name: sort
in: query
schema:
type: string
enum: [created_at, "-created_at", last_used_at, "-last_used_at", expires_at, "-expires_at"]
- name: user_id
in: query
schema: { type: string, format: uuid }
description: Фильтр по владельцу токена.
- name: name
in: query
schema: { type: string, maxLength: 100 }
description: Like-поиск по названию токена.
- name: token_prefix
in: query
schema: { type: string, maxLength: 16 }
description: 'Like-поиск по префиксу (например `hrb_abc12345`).'
- name: status_id
in: query
schema: { type: string }
description: |
Comma-separated `UserApiTokenStatus`: `1` — Активен, `2` — Отозван,
`3` — Истёк. Без параметра — все статусы.
responses:
200:
description: Список
content:
application/json:
schema:
$ref: '#/components/schemas/UserApiTokenAdminList'
403:
description: Запрос под Bearer, либо нет права `api-tokens-admin`.
/admin/user-api-token/revoke:
post:
tags: [admin-user-api-token]
summary: Отозвать любой токен тенанта
description: |
Идемпотентно. Доступно только под session-авторизацией (Bearer → 403).
operationId: adminUserApiTokenRevoke
security:
- SessionAuth: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required: [id]
properties:
id:
type: string
format: uuid
responses:
200:
description: Успех
content:
application/json:
schema:
$ref: '#/components/schemas/httpJsonResponse'
400:
description: Параметр `id` не строка или не uuid.
403:
description: Запрос под Bearer, либо нет права `api-tokens-admin`.
404:
description: Токен не найден.
components:
securitySchemes:
SessionAuth:
@@ -7277,6 +7562,16 @@ components:
in: header
name: X-Hrbox-Client-Id
description: "Авторизация с использованием API-ключа. Требуемые заголовки: Content-Type, X-Hrbox-Client-Id, X-Hrbox-Client-Secret."
BearerAuth:
type: http
scheme: bearer
bearerFormat: PAT
description: |
Personal Access Token (постоянный API-токен пользователя). Заголовок:
`Authorization: Bearer hrb_<32 url-safe>`. Tenant авторезолвится по
`Host`, `x-hrbox-tenant-id` опционален. Работает только на `/api/v2/*`.
Токен наследует все права юзера, под которым выпущен (granular scopes
не поддерживаются). Подробности — в описании API.
schemas:
httpJsonResponse:
type: object
@@ -12084,6 +12379,8 @@ components:
type: array
items:
$ref: '#/components/schemas/Wallet'
_links:
$ref: '#/components/schemas/_links'
_meta:
$ref: '#/components/schemas/_meta'
@@ -12116,6 +12413,8 @@ components:
type: array
items:
$ref: '#/components/schemas/WalletTransaction'
_links:
$ref: '#/components/schemas/_links'
_meta:
$ref: '#/components/schemas/_meta'
@@ -12195,10 +12494,13 @@ components:
1 — NEW, 2 — QUEUED, 3 — STARTED, 4 — FINISHED, 5 — ERROR, 6 — FATAL
arguments:
type: array
result:
type: object
description: |
Результат. Для wallet-excel содержит:
Аргументы воркера. Для wallet-excel: `[WalletExcelProcessor::class, mappings]`.
Для excel-экспорта: `[ReportClass::class, params]`.
items: {}
result:
description: |
Результат. Для wallet-excel:
```
{
"summary": { "totalRows": 4, "validCount": 1, "invalidCount": 3 },
@@ -12206,9 +12508,32 @@ components:
"invalidRows": [{ "row": 2, "errors": {...} }, ...]
}
```
Для незавершённой обработки или экспорта — пустой массив `[]`.
oneOf:
- type: object
- type: array
items: {}
created_at: { type: string }
started_at: { type: string, nullable: true }
finished_at: { type: string, nullable: true }
file:
type: object
nullable: true
description: |
Метаданные файла-источника. Возвращается всегда (не нужно `?expand=`).
Содержит `id, name, ext, fid, url, file_type, mime_type, size,
file_status_id, user_id, created_at` и др.
created:
type: string
description: 'Локализованная `created_at` (например `04.05.2026 at 13:56`).'
started:
type: string
nullable: true
description: Локализованная `started_at`.
finished:
type: string
nullable: true
description: Локализованная `finished_at`.
FileProcessingList:
type: object
@@ -12217,6 +12542,134 @@ components:
type: array
items:
$ref: '#/components/schemas/FileProcessing'
_links:
$ref: '#/components/schemas/_links'
_meta:
$ref: '#/components/schemas/_meta'
UserApiTokenStatus:
type: integer
description: |
Статус личного API-токена (вычисляется по `revoked_at` / `expires_at`):
* `1` — Активен
* `2` — Отозван
* `3` — Истёк
enum: [1, 2, 3]
example: 1
UserApiToken:
type: object
description: |
Личный API-токен (PAT). Plain-секрет в этой схеме отсутствует — он
возвращается только в ответе на создание (см. `UserApiTokenCreated`).
properties:
id: { type: string, format: uuid }
user_id: { type: string, format: uuid }
name:
type: string
maxLength: 100
description: Человекочитаемое название от юзера (HTML-теги вырезаются на бэке).
token_prefix:
type: string
description: |
Первые 12 символов токена с приставкой `hrb_`. Безопасно для отображения
и поиска (например `hrb_abc12345`).
example: "hrb_abc12345"
created_at: { type: string, format: date-time }
expires_at:
type: string
format: date-time
nullable: true
description: Если null — без срока. Срок включает указанный день целиком (до 23:59:59).
last_used_at:
type: string
format: date-time
nullable: true
description: Обновляется при использовании, не чаще раза в минуту.
revoked_at:
type: string
format: date-time
nullable: true
description: Заполняется при revoke; если null — токен не отозван.
UserApiTokenList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/UserApiToken'
_links:
$ref: '#/components/schemas/_links'
_meta:
$ref: '#/components/schemas/_meta'
UserApiTokenCreated:
description: |
Расширение `UserApiToken` дополнительным полем `plain` — это
единственный момент, когда секрет возвращается клиенту.
allOf:
- $ref: '#/components/schemas/UserApiToken'
- type: object
required: [plain]
properties:
plain:
type: string
description: |
Полный plain-токен. Формат `hrb_<32 url-safe>`. **Сохраните
немедленно — повторно его получить невозможно.**
example: "hrb_aBcD3fGhIjKlMnOpQrStUvWxYz012345"
UserApiTokenCreateForm:
type: object
required: [name]
properties:
name:
type: string
maxLength: 100
description: Название токена. Сохраняется после HTML-purify.
example: "Интеграция web-zaim — wallet"
expires_at:
type: string
format: date
nullable: true
description: |
Дата истечения `yyyy-MM-dd`. Не может быть в прошлом (валидируется
по началу текущего дня). На бэке к ней приклеивается время
`23:59:59`, токен работает весь указанный день включительно.
Если опущено — токен бессрочный.
example: "2026-12-31"
UserApiTokenAdmin:
description: |
Запись токена в админском списке: к базовым полям добавляются вложенные
`user` и `updatedByUser` (последний — кто последним менял запись;
для авто-revoke при блокировке — система).
allOf:
- $ref: '#/components/schemas/UserApiToken'
- type: object
properties:
user:
type: object
properties:
id: { type: string, format: uuid }
name: { type: string }
updatedByUser:
type: object
nullable: true
properties:
id: { type: string, format: uuid }
name: { type: string }
UserApiTokenAdminList:
type: object
properties:
data:
type: array
items:
$ref: '#/components/schemas/UserApiTokenAdmin'
_links:
$ref: '#/components/schemas/_links'
_meta:
$ref: '#/components/schemas/_meta'
@@ -12406,3 +12859,4 @@ components:
security:
- SessionAuth: [ ]
- ApiKeyAuth: [ ]
- BearerAuth: [ ]