feat(api): user management and audit log for admins (M1)
- /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:
1 parent
ed4a492dba
commit
f075863f15
13 files changed
+2578
-70
No files matched your search
@@ -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]
|
||||
|
||||
Reference in new issue
Block a user