feat(api): user management and audit log for admins (M1)
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

- /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>
This commit is contained in:
mrleo1nidandClaude Opus 5.5 committed 2026-10-20 06:18:02 +03:00
1 parent ed4a492dba
commit f075863f15
13 files changed
+2578 -70

No files matched your search

+266
View File
@@ -11,6 +11,7 @@ tags:
- name: system
- name: auth
- name: me
- name: admin
security:
- bearerAuth: []
paths:
@@ -251,7 +252,146 @@ paths:
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
@@ -429,6 +569,132 @@ components:
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]