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
@@ -234,6 +234,94 @@ export interface paths {
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/admin/users": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/**
|
||||
* Пользователи (admin, moderator)
|
||||
* @description Сортировка по логину. `q` ищет по логину и имени без учёта регистра.
|
||||
*/
|
||||
get: operations["listUsers"];
|
||||
put?: never;
|
||||
/**
|
||||
* Создать пользователя
|
||||
* @description Пароль генерирует сервер и возвращает **один раз**; при первом входе его нужно сменить.
|
||||
* Модератор создаёт только `user`, админ — любые роли.
|
||||
*/
|
||||
post: operations["createUser"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/admin/users/{id}": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: components["parameters"]["UserId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
post?: never;
|
||||
/** Удалить пользователя (admin) */
|
||||
delete: operations["deleteUser"];
|
||||
options?: never;
|
||||
head?: never;
|
||||
/**
|
||||
* Изменить имя, роль, блокировку
|
||||
* @description Роль меняет только админ. Смена роли и блокировка мгновенно отзывают токены пользователя.
|
||||
* Последнего активного админа нельзя понизить или заблокировать — `409 users.last_admin`.
|
||||
*/
|
||||
patch: operations["updateUser"];
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/admin/users/{id}/reset-password": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: components["parameters"]["UserId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
get?: never;
|
||||
put?: never;
|
||||
/**
|
||||
* Сбросить пароль
|
||||
* @description Новый временный пароль возвращается один раз; все сессии пользователя завершаются.
|
||||
*/
|
||||
post: operations["resetUserPassword"];
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
"/api/v1/admin/audit": {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
/** Журнал действий (admin), новые сверху */
|
||||
get: operations["listAudit"];
|
||||
put?: never;
|
||||
post?: never;
|
||||
delete?: never;
|
||||
options?: never;
|
||||
head?: never;
|
||||
patch?: never;
|
||||
trace?: never;
|
||||
};
|
||||
}
|
||||
export type webhooks = Record<string, never>;
|
||||
export interface components {
|
||||
@@ -340,6 +428,69 @@ export interface components {
|
||||
new_username: string;
|
||||
new_password: string;
|
||||
};
|
||||
AdminUser: {
|
||||
/** Format: uuid */
|
||||
id: string;
|
||||
username: string;
|
||||
display_name: string | null;
|
||||
role: components["schemas"]["Role"];
|
||||
locale: string;
|
||||
is_blocked: boolean;
|
||||
must_change_password: boolean;
|
||||
must_change_credentials: boolean;
|
||||
/** Format: date-time */
|
||||
created_at: string;
|
||||
/** Format: date-time */
|
||||
last_login_at: string | null;
|
||||
};
|
||||
AdminUserPage: {
|
||||
items: components["schemas"]["AdminUser"][];
|
||||
next_cursor: string | null;
|
||||
};
|
||||
CreateUserRequest: {
|
||||
username: string;
|
||||
display_name?: string;
|
||||
role: components["schemas"]["Role"];
|
||||
};
|
||||
CreatedUser: {
|
||||
user: components["schemas"]["AdminUser"];
|
||||
/** @description Показывается один раз */
|
||||
temporary_password: string;
|
||||
};
|
||||
UpdateUserRequest: {
|
||||
/** @description Пустая строка — сбросить имя */
|
||||
display_name?: string;
|
||||
role?: components["schemas"]["Role"];
|
||||
is_blocked?: boolean;
|
||||
};
|
||||
TemporaryPassword: {
|
||||
temporary_password: string;
|
||||
};
|
||||
AuditActor: {
|
||||
/** Format: uuid */
|
||||
id: string;
|
||||
username: string;
|
||||
};
|
||||
AuditEntry: {
|
||||
/** Format: uuid */
|
||||
id: string;
|
||||
/** @description null — система или удалённый пользователь */
|
||||
actor: components["schemas"]["AuditActor"] | null;
|
||||
/** @example user.create */
|
||||
action: string;
|
||||
target_type: string | null;
|
||||
/** Format: uuid */
|
||||
target_id: string | null;
|
||||
payload: {
|
||||
[key: string]: unknown;
|
||||
} | null;
|
||||
/** Format: date-time */
|
||||
created_at: string;
|
||||
};
|
||||
AuditPage: {
|
||||
items: components["schemas"]["AuditEntry"][];
|
||||
next_cursor: string | null;
|
||||
};
|
||||
Session: {
|
||||
/** Format: uuid */
|
||||
id: string;
|
||||
@@ -388,7 +539,12 @@ export interface components {
|
||||
};
|
||||
};
|
||||
};
|
||||
parameters: never;
|
||||
parameters: {
|
||||
Limit: number;
|
||||
/** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */
|
||||
Cursor: string;
|
||||
UserId: string;
|
||||
};
|
||||
requestBodies: never;
|
||||
headers: never;
|
||||
pathItems: never;
|
||||
@@ -693,4 +849,152 @@ export interface operations {
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
listUsers: {
|
||||
parameters: {
|
||||
query?: {
|
||||
q?: string;
|
||||
role?: components["schemas"]["Role"];
|
||||
limit?: components["parameters"]["Limit"];
|
||||
/** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Страница пользователей */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminUserPage"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
createUser: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreateUserRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Пользователь создан */
|
||||
201: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["CreatedUser"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
deleteUser: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: components["parameters"]["UserId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Удалён */
|
||||
204: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content?: never;
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
updateUser: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: components["parameters"]["UserId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody: {
|
||||
content: {
|
||||
"application/json": components["schemas"]["UpdateUserRequest"];
|
||||
};
|
||||
};
|
||||
responses: {
|
||||
/** @description Обновлённый пользователь */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AdminUser"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
resetUserPassword: {
|
||||
parameters: {
|
||||
query?: never;
|
||||
header?: never;
|
||||
path: {
|
||||
id: components["parameters"]["UserId"];
|
||||
};
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Временный пароль */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["TemporaryPassword"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
listAudit: {
|
||||
parameters: {
|
||||
query?: {
|
||||
limit?: components["parameters"]["Limit"];
|
||||
/** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */
|
||||
cursor?: components["parameters"]["Cursor"];
|
||||
};
|
||||
header?: never;
|
||||
path?: never;
|
||||
cookie?: never;
|
||||
};
|
||||
requestBody?: never;
|
||||
responses: {
|
||||
/** @description Страница журнала */
|
||||
200: {
|
||||
headers: {
|
||||
[name: string]: unknown;
|
||||
};
|
||||
content: {
|
||||
"application/json": components["schemas"]["AuditPage"];
|
||||
};
|
||||
};
|
||||
default: components["responses"]["Problem"];
|
||||
};
|
||||
};
|
||||
}
|
||||
Reference in new issue
Block a user