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: system
|
||||||
- name: auth
|
- name: auth
|
||||||
- name: me
|
- name: me
|
||||||
|
- name: admin
|
||||||
security:
|
security:
|
||||||
- bearerAuth: []
|
- bearerAuth: []
|
||||||
paths:
|
paths:
|
||||||
@@ -251,7 +252,146 @@ paths:
|
|||||||
description: Сессия завершена
|
description: Сессия завершена
|
||||||
default:
|
default:
|
||||||
$ref: "#/components/responses/Problem"
|
$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:
|
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:
|
securitySchemes:
|
||||||
bearerAuth:
|
bearerAuth:
|
||||||
type: http
|
type: http
|
||||||
@@ -429,6 +569,132 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
new_password:
|
new_password:
|
||||||
type: string
|
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:
|
Session:
|
||||||
type: object
|
type: object
|
||||||
required: [id, device_name, client_type, created_at, last_used_at, current]
|
required: [id, device_name, client_type, created_at, last_used_at, current]
|
||||||
|
|||||||
@@ -234,6 +234,94 @@ export interface paths {
|
|||||||
patch?: never;
|
patch?: never;
|
||||||
trace?: 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 type webhooks = Record<string, never>;
|
||||||
export interface components {
|
export interface components {
|
||||||
@@ -340,6 +428,69 @@ export interface components {
|
|||||||
new_username: string;
|
new_username: string;
|
||||||
new_password: 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: {
|
Session: {
|
||||||
/** Format: uuid */
|
/** Format: uuid */
|
||||||
id: string;
|
id: string;
|
||||||
@@ -388,7 +539,12 @@ export interface components {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
parameters: never;
|
parameters: {
|
||||||
|
Limit: number;
|
||||||
|
/** @description Непрозрачный курсор из `next_cursor` предыдущей страницы */
|
||||||
|
Cursor: string;
|
||||||
|
UserId: string;
|
||||||
|
};
|
||||||
requestBodies: never;
|
requestBodies: never;
|
||||||
headers: never;
|
headers: never;
|
||||||
pathItems: never;
|
pathItems: never;
|
||||||
@@ -693,4 +849,152 @@ export interface operations {
|
|||||||
default: components["responses"]["Problem"];
|
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"];
|
||||||
|
};
|
||||||
|
};
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"current_password_required": "Enter your current password",
|
"current_password_required": "Enter your current password",
|
||||||
"current_password_invalid": "The current password is wrong",
|
"current_password_invalid": "The current password is wrong",
|
||||||
"bootstrap_not_required": "The administrator credentials have already been changed",
|
"bootstrap_not_required": "The administrator credentials have already been changed",
|
||||||
"session_not_found": "Session not found"
|
"session_not_found": "Session not found",
|
||||||
|
"forbidden": "You don't have permission to do this"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"password_too_short": "The password must be at least {{min}} characters",
|
"password_too_short": "The password must be at least {{min}} characters",
|
||||||
@@ -34,6 +35,12 @@
|
|||||||
"locale_unsupported": "This language is not supported",
|
"locale_unsupported": "This language is not supported",
|
||||||
"display_name_too_long": "The name must be at most {{max}} characters",
|
"display_name_too_long": "The name must be at most {{max}} characters",
|
||||||
"ui_prefs_invalid": "Invalid setting value",
|
"ui_prefs_invalid": "Invalid setting value",
|
||||||
"client_type_invalid": "Unknown app type"
|
"client_type_invalid": "Unknown app type",
|
||||||
|
"role_invalid": "Unknown role",
|
||||||
|
"cursor_invalid": "The list is outdated, please reload"
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"not_found": "User not found",
|
||||||
|
"last_admin": "The last administrator cannot be demoted, blocked or deleted"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -21,7 +21,8 @@
|
|||||||
"current_password_required": "Введите текущий пароль",
|
"current_password_required": "Введите текущий пароль",
|
||||||
"current_password_invalid": "Текущий пароль указан неверно",
|
"current_password_invalid": "Текущий пароль указан неверно",
|
||||||
"bootstrap_not_required": "Учётные данные администратора уже изменены",
|
"bootstrap_not_required": "Учётные данные администратора уже изменены",
|
||||||
"session_not_found": "Сессия не найдена"
|
"session_not_found": "Сессия не найдена",
|
||||||
|
"forbidden": "Недостаточно прав для этого действия"
|
||||||
},
|
},
|
||||||
"validation": {
|
"validation": {
|
||||||
"password_too_short": "Пароль должен быть не короче {{min}} символов",
|
"password_too_short": "Пароль должен быть не короче {{min}} символов",
|
||||||
@@ -34,6 +35,12 @@
|
|||||||
"locale_unsupported": "Этот язык не поддерживается",
|
"locale_unsupported": "Этот язык не поддерживается",
|
||||||
"display_name_too_long": "Имя должно быть не длиннее {{max}} символов",
|
"display_name_too_long": "Имя должно быть не длиннее {{max}} символов",
|
||||||
"ui_prefs_invalid": "Недопустимое значение настройки",
|
"ui_prefs_invalid": "Недопустимое значение настройки",
|
||||||
"client_type_invalid": "Неизвестный тип приложения"
|
"client_type_invalid": "Неизвестный тип приложения",
|
||||||
|
"role_invalid": "Неизвестная роль",
|
||||||
|
"cursor_invalid": "Список устарел, обновите страницу"
|
||||||
|
},
|
||||||
|
"users": {
|
||||||
|
"not_found": "Пользователь не найден",
|
||||||
|
"last_admin": "Нельзя понизить, заблокировать или удалить последнего администратора"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -81,6 +81,51 @@ const (
|
|||||||
Island UiPrefsWebPlayer = "island"
|
Island UiPrefsWebPlayer = "island"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// AdminUser defines model for AdminUser.
|
||||||
|
type AdminUser struct {
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
DisplayName *string `json:"display_name"`
|
||||||
|
Id openapi_types.UUID `json:"id"`
|
||||||
|
IsBlocked bool `json:"is_blocked"`
|
||||||
|
LastLoginAt *time.Time `json:"last_login_at"`
|
||||||
|
Locale string `json:"locale"`
|
||||||
|
MustChangeCredentials bool `json:"must_change_credentials"`
|
||||||
|
MustChangePassword bool `json:"must_change_password"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUserPage defines model for AdminUserPage.
|
||||||
|
type AdminUserPage struct {
|
||||||
|
Items []AdminUser `json:"items"`
|
||||||
|
NextCursor *string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditActor defines model for AuditActor.
|
||||||
|
type AuditActor struct {
|
||||||
|
Id openapi_types.UUID `json:"id"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditEntry defines model for AuditEntry.
|
||||||
|
type AuditEntry struct {
|
||||||
|
Action string `json:"action"`
|
||||||
|
|
||||||
|
// Actor null — система или удалённый пользователь
|
||||||
|
Actor *AuditActor `json:"actor"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
|
Id openapi_types.UUID `json:"id"`
|
||||||
|
Payload *map[string]interface{} `json:"payload"`
|
||||||
|
TargetId *openapi_types.UUID `json:"target_id"`
|
||||||
|
TargetType *string `json:"target_type"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// AuditPage defines model for AuditPage.
|
||||||
|
type AuditPage struct {
|
||||||
|
Items []AuditEntry `json:"items"`
|
||||||
|
NextCursor *string `json:"next_cursor"`
|
||||||
|
}
|
||||||
|
|
||||||
// AuthTokens defines model for AuthTokens.
|
// AuthTokens defines model for AuthTokens.
|
||||||
type AuthTokens struct {
|
type AuthTokens struct {
|
||||||
AccessToken string `json:"access_token"`
|
AccessToken string `json:"access_token"`
|
||||||
@@ -112,6 +157,20 @@ type ChangePasswordRequest struct {
|
|||||||
// ClientType defines model for ClientType.
|
// ClientType defines model for ClientType.
|
||||||
type ClientType string
|
type ClientType string
|
||||||
|
|
||||||
|
// CreateUserRequest defines model for CreateUserRequest.
|
||||||
|
type CreateUserRequest struct {
|
||||||
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
|
Role Role `json:"role"`
|
||||||
|
Username string `json:"username"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreatedUser defines model for CreatedUser.
|
||||||
|
type CreatedUser struct {
|
||||||
|
// TemporaryPassword Показывается один раз
|
||||||
|
TemporaryPassword string `json:"temporary_password"`
|
||||||
|
User AdminUser `json:"user"`
|
||||||
|
}
|
||||||
|
|
||||||
// FieldError defines model for FieldError.
|
// FieldError defines model for FieldError.
|
||||||
type FieldError struct {
|
type FieldError struct {
|
||||||
Code string `json:"code"`
|
Code string `json:"code"`
|
||||||
@@ -218,6 +277,11 @@ type SessionList struct {
|
|||||||
Items []Session `json:"items"`
|
Items []Session `json:"items"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// TemporaryPassword defines model for TemporaryPassword.
|
||||||
|
type TemporaryPassword struct {
|
||||||
|
TemporaryPassword string `json:"temporary_password"`
|
||||||
|
}
|
||||||
|
|
||||||
// UiPrefs defines model for UiPrefs.
|
// UiPrefs defines model for UiPrefs.
|
||||||
type UiPrefs struct {
|
type UiPrefs struct {
|
||||||
Ambient *bool `json:"ambient,omitempty"`
|
Ambient *bool `json:"ambient,omitempty"`
|
||||||
@@ -249,11 +313,52 @@ type UpdateMeRequest struct {
|
|||||||
UiPrefs *UiPrefs `json:"ui_prefs,omitempty"`
|
UiPrefs *UiPrefs `json:"ui_prefs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UpdateUserRequest defines model for UpdateUserRequest.
|
||||||
|
type UpdateUserRequest struct {
|
||||||
|
// DisplayName Пустая строка — сбросить имя
|
||||||
|
DisplayName *string `json:"display_name,omitempty"`
|
||||||
|
IsBlocked *bool `json:"is_blocked,omitempty"`
|
||||||
|
Role *Role `json:"role,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cursor defines model for Cursor.
|
||||||
|
type Cursor = string
|
||||||
|
|
||||||
|
// Limit defines model for Limit.
|
||||||
|
type Limit = int
|
||||||
|
|
||||||
|
// UserId defines model for UserId.
|
||||||
|
type UserId = openapi_types.UUID
|
||||||
|
|
||||||
|
// ListAuditParams defines parameters for ListAudit.
|
||||||
|
type ListAuditParams struct {
|
||||||
|
Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"`
|
||||||
|
|
||||||
|
// Cursor Непрозрачный курсор из `next_cursor` предыдущей страницы
|
||||||
|
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsersParams defines parameters for ListUsers.
|
||||||
|
type ListUsersParams struct {
|
||||||
|
Q *string `form:"q,omitempty" json:"q,omitempty"`
|
||||||
|
Role *Role `form:"role,omitempty" json:"role,omitempty"`
|
||||||
|
Limit *Limit `form:"limit,omitempty" json:"limit,omitempty"`
|
||||||
|
|
||||||
|
// Cursor Непрозрачный курсор из `next_cursor` предыдущей страницы
|
||||||
|
Cursor *Cursor `form:"cursor,omitempty" json:"cursor,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
// RefreshTokensParams defines parameters for RefreshTokens.
|
// RefreshTokensParams defines parameters for RefreshTokens.
|
||||||
type RefreshTokensParams struct {
|
type RefreshTokensParams struct {
|
||||||
PipuRt *string `form:"pipu_rt,omitempty" json:"pipu_rt,omitempty"`
|
PipuRt *string `form:"pipu_rt,omitempty" json:"pipu_rt,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// CreateUserJSONRequestBody defines body for CreateUser for application/json ContentType.
|
||||||
|
type CreateUserJSONRequestBody = CreateUserRequest
|
||||||
|
|
||||||
|
// UpdateUserJSONRequestBody defines body for UpdateUser for application/json ContentType.
|
||||||
|
type UpdateUserJSONRequestBody = UpdateUserRequest
|
||||||
|
|
||||||
// LoginJSONRequestBody defines body for Login for application/json ContentType.
|
// LoginJSONRequestBody defines body for Login for application/json ContentType.
|
||||||
type LoginJSONRequestBody = LoginRequest
|
type LoginJSONRequestBody = LoginRequest
|
||||||
|
|
||||||
@@ -271,6 +376,24 @@ type ChangePasswordJSONRequestBody = ChangePasswordRequest
|
|||||||
|
|
||||||
// ServerInterface represents all server handlers.
|
// ServerInterface represents all server handlers.
|
||||||
type ServerInterface interface {
|
type ServerInterface interface {
|
||||||
|
// Журнал действий (admin), новые сверху
|
||||||
|
// (GET /api/v1/admin/audit)
|
||||||
|
ListAudit(w http.ResponseWriter, r *http.Request, params ListAuditParams)
|
||||||
|
// Пользователи (admin, moderator)
|
||||||
|
// (GET /api/v1/admin/users)
|
||||||
|
ListUsers(w http.ResponseWriter, r *http.Request, params ListUsersParams)
|
||||||
|
// Создать пользователя
|
||||||
|
// (POST /api/v1/admin/users)
|
||||||
|
CreateUser(w http.ResponseWriter, r *http.Request)
|
||||||
|
// Удалить пользователя (admin)
|
||||||
|
// (DELETE /api/v1/admin/users/{id})
|
||||||
|
DeleteUser(w http.ResponseWriter, r *http.Request, id UserId)
|
||||||
|
// Изменить имя, роль, блокировку
|
||||||
|
// (PATCH /api/v1/admin/users/{id})
|
||||||
|
UpdateUser(w http.ResponseWriter, r *http.Request, id UserId)
|
||||||
|
// Сбросить пароль
|
||||||
|
// (POST /api/v1/admin/users/{id}/reset-password)
|
||||||
|
ResetUserPassword(w http.ResponseWriter, r *http.Request, id UserId)
|
||||||
// Вход по логину и паролю
|
// Вход по логину и паролю
|
||||||
// (POST /api/v1/auth/login)
|
// (POST /api/v1/auth/login)
|
||||||
Login(w http.ResponseWriter, r *http.Request)
|
Login(w http.ResponseWriter, r *http.Request)
|
||||||
@@ -316,6 +439,42 @@ type ServerInterface interface {
|
|||||||
|
|
||||||
type Unimplemented struct{}
|
type Unimplemented struct{}
|
||||||
|
|
||||||
|
// Журнал действий (admin), новые сверху
|
||||||
|
// (GET /api/v1/admin/audit)
|
||||||
|
func (_ Unimplemented) ListAudit(w http.ResponseWriter, r *http.Request, params ListAuditParams) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Пользователи (admin, moderator)
|
||||||
|
// (GET /api/v1/admin/users)
|
||||||
|
func (_ Unimplemented) ListUsers(w http.ResponseWriter, r *http.Request, params ListUsersParams) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Создать пользователя
|
||||||
|
// (POST /api/v1/admin/users)
|
||||||
|
func (_ Unimplemented) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Удалить пользователя (admin)
|
||||||
|
// (DELETE /api/v1/admin/users/{id})
|
||||||
|
func (_ Unimplemented) DeleteUser(w http.ResponseWriter, r *http.Request, id UserId) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Изменить имя, роль, блокировку
|
||||||
|
// (PATCH /api/v1/admin/users/{id})
|
||||||
|
func (_ Unimplemented) UpdateUser(w http.ResponseWriter, r *http.Request, id UserId) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сбросить пароль
|
||||||
|
// (POST /api/v1/admin/users/{id}/reset-password)
|
||||||
|
func (_ Unimplemented) ResetUserPassword(w http.ResponseWriter, r *http.Request, id UserId) {
|
||||||
|
w.WriteHeader(http.StatusNotImplemented)
|
||||||
|
}
|
||||||
|
|
||||||
// Вход по логину и паролю
|
// Вход по логину и паролю
|
||||||
// (POST /api/v1/auth/login)
|
// (POST /api/v1/auth/login)
|
||||||
func (_ Unimplemented) Login(w http.ResponseWriter, r *http.Request) {
|
func (_ Unimplemented) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -403,6 +562,217 @@ type ServerInterfaceWrapper struct {
|
|||||||
|
|
||||||
type MiddlewareFunc func(http.Handler) http.Handler
|
type MiddlewareFunc func(http.Handler) http.Handler
|
||||||
|
|
||||||
|
// ListAudit operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) ListAudit(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// Parameter object where we will unmarshal all parameters from the context
|
||||||
|
var params ListAuditParams
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "limit" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "cursor" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "cursor", r.URL.Query(), ¶ms.Cursor)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "cursor", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.ListAudit(w, r, params)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) ListUsers(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
// Parameter object where we will unmarshal all parameters from the context
|
||||||
|
var params ListUsersParams
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "q" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "q", r.URL.Query(), ¶ms.Q)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "q", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "role" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "role", r.URL.Query(), ¶ms.Role)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "role", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "limit" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "limit", r.URL.Query(), ¶ms.Limit)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "limit", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// ------------- Optional query parameter "cursor" -------------
|
||||||
|
|
||||||
|
err = runtime.BindQueryParameter("form", true, false, "cursor", r.URL.Query(), ¶ms.Cursor)
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "cursor", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.ListUsers(w, r, params)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.CreateUser(w, r)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) DeleteUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// ------------- Path parameter "id" -------------
|
||||||
|
var id UserId
|
||||||
|
|
||||||
|
err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.DeleteUser(w, r, id)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) UpdateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// ------------- Path parameter "id" -------------
|
||||||
|
var id UserId
|
||||||
|
|
||||||
|
err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.UpdateUser(w, r, id)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword operation middleware
|
||||||
|
func (siw *ServerInterfaceWrapper) ResetUserPassword(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
|
var err error
|
||||||
|
|
||||||
|
// ------------- Path parameter "id" -------------
|
||||||
|
var id UserId
|
||||||
|
|
||||||
|
err = runtime.BindStyledParameterWithOptions("simple", "id", chi.URLParam(r, "id"), &id, runtime.BindStyledParameterOptions{ParamLocation: runtime.ParamLocationPath, Explode: false, Required: true})
|
||||||
|
if err != nil {
|
||||||
|
siw.ErrorHandlerFunc(w, r, &InvalidParamFormatError{ParamName: "id", Err: err})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := r.Context()
|
||||||
|
|
||||||
|
ctx = context.WithValue(ctx, BearerAuthScopes, []string{})
|
||||||
|
|
||||||
|
r = r.WithContext(ctx)
|
||||||
|
|
||||||
|
handler := http.Handler(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
siw.Handler.ResetUserPassword(w, r, id)
|
||||||
|
}))
|
||||||
|
|
||||||
|
for _, middleware := range siw.HandlerMiddlewares {
|
||||||
|
handler = middleware(handler)
|
||||||
|
}
|
||||||
|
|
||||||
|
handler.ServeHTTP(w, r)
|
||||||
|
}
|
||||||
|
|
||||||
// Login operation middleware
|
// Login operation middleware
|
||||||
func (siw *ServerInterfaceWrapper) Login(w http.ResponseWriter, r *http.Request) {
|
func (siw *ServerInterfaceWrapper) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
|
|
||||||
@@ -777,6 +1147,24 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
|
|||||||
ErrorHandlerFunc: options.ErrorHandlerFunc,
|
ErrorHandlerFunc: options.ErrorHandlerFunc,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Get(options.BaseURL+"/api/v1/admin/audit", wrapper.ListAudit)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Get(options.BaseURL+"/api/v1/admin/users", wrapper.ListUsers)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Post(options.BaseURL+"/api/v1/admin/users", wrapper.CreateUser)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Delete(options.BaseURL+"/api/v1/admin/users/{id}", wrapper.DeleteUser)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Patch(options.BaseURL+"/api/v1/admin/users/{id}", wrapper.UpdateUser)
|
||||||
|
})
|
||||||
|
r.Group(func(r chi.Router) {
|
||||||
|
r.Post(options.BaseURL+"/api/v1/admin/users/{id}/reset-password", wrapper.ResetUserPassword)
|
||||||
|
})
|
||||||
r.Group(func(r chi.Router) {
|
r.Group(func(r chi.Router) {
|
||||||
r.Post(options.BaseURL+"/api/v1/auth/login", wrapper.Login)
|
r.Post(options.BaseURL+"/api/v1/auth/login", wrapper.Login)
|
||||||
})
|
})
|
||||||
@@ -822,6 +1210,180 @@ func HandlerWithOptions(si ServerInterface, options ChiServerOptions) http.Handl
|
|||||||
|
|
||||||
type ProblemApplicationProblemPlusJSONResponse Problem
|
type ProblemApplicationProblemPlusJSONResponse Problem
|
||||||
|
|
||||||
|
type ListAuditRequestObject struct {
|
||||||
|
Params ListAuditParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListAuditResponseObject interface {
|
||||||
|
VisitListAuditResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListAudit200JSONResponse AuditPage
|
||||||
|
|
||||||
|
func (response ListAudit200JSONResponse) VisitListAuditResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListAuditdefaultApplicationProblemPlusJSONResponse struct {
|
||||||
|
Body Problem
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response ListAuditdefaultApplicationProblemPlusJSONResponse) VisitListAuditResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/problem+json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUsersRequestObject struct {
|
||||||
|
Params ListUsersParams
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUsersResponseObject interface {
|
||||||
|
VisitListUsersResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUsers200JSONResponse AdminUserPage
|
||||||
|
|
||||||
|
func (response ListUsers200JSONResponse) VisitListUsersResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ListUsersdefaultApplicationProblemPlusJSONResponse struct {
|
||||||
|
Body Problem
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response ListUsersdefaultApplicationProblemPlusJSONResponse) VisitListUsersResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/problem+json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserRequestObject struct {
|
||||||
|
Body *CreateUserJSONRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserResponseObject interface {
|
||||||
|
VisitCreateUserResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUser201JSONResponse CreatedUser
|
||||||
|
|
||||||
|
func (response CreateUser201JSONResponse) VisitCreateUserResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(201)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type CreateUserdefaultApplicationProblemPlusJSONResponse struct {
|
||||||
|
Body Problem
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response CreateUserdefaultApplicationProblemPlusJSONResponse) VisitCreateUserResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/problem+json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteUserRequestObject struct {
|
||||||
|
Id UserId `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteUserResponseObject interface {
|
||||||
|
VisitDeleteUserResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteUser204Response struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response DeleteUser204Response) VisitDeleteUserResponse(w http.ResponseWriter) error {
|
||||||
|
w.WriteHeader(204)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type DeleteUserdefaultApplicationProblemPlusJSONResponse struct {
|
||||||
|
Body Problem
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response DeleteUserdefaultApplicationProblemPlusJSONResponse) VisitDeleteUserResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/problem+json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserRequestObject struct {
|
||||||
|
Id UserId `json:"id"`
|
||||||
|
Body *UpdateUserJSONRequestBody
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserResponseObject interface {
|
||||||
|
VisitUpdateUserResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUser200JSONResponse AdminUser
|
||||||
|
|
||||||
|
func (response UpdateUser200JSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type UpdateUserdefaultApplicationProblemPlusJSONResponse struct {
|
||||||
|
Body Problem
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response UpdateUserdefaultApplicationProblemPlusJSONResponse) VisitUpdateUserResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/problem+json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetUserPasswordRequestObject struct {
|
||||||
|
Id UserId `json:"id"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetUserPasswordResponseObject interface {
|
||||||
|
VisitResetUserPasswordResponse(w http.ResponseWriter) error
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetUserPassword200JSONResponse TemporaryPassword
|
||||||
|
|
||||||
|
func (response ResetUserPassword200JSONResponse) VisitResetUserPasswordResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
w.WriteHeader(200)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response)
|
||||||
|
}
|
||||||
|
|
||||||
|
type ResetUserPassworddefaultApplicationProblemPlusJSONResponse struct {
|
||||||
|
Body Problem
|
||||||
|
StatusCode int
|
||||||
|
}
|
||||||
|
|
||||||
|
func (response ResetUserPassworddefaultApplicationProblemPlusJSONResponse) VisitResetUserPasswordResponse(w http.ResponseWriter) error {
|
||||||
|
w.Header().Set("Content-Type", "application/problem+json")
|
||||||
|
w.WriteHeader(response.StatusCode)
|
||||||
|
|
||||||
|
return json.NewEncoder(w).Encode(response.Body)
|
||||||
|
}
|
||||||
|
|
||||||
type LoginRequestObject struct {
|
type LoginRequestObject struct {
|
||||||
Body *LoginJSONRequestBody
|
Body *LoginJSONRequestBody
|
||||||
}
|
}
|
||||||
@@ -1177,6 +1739,24 @@ func (response GetReadyz503JSONResponse) VisitGetReadyzResponse(w http.ResponseW
|
|||||||
|
|
||||||
// StrictServerInterface represents all server handlers.
|
// StrictServerInterface represents all server handlers.
|
||||||
type StrictServerInterface interface {
|
type StrictServerInterface interface {
|
||||||
|
// Журнал действий (admin), новые сверху
|
||||||
|
// (GET /api/v1/admin/audit)
|
||||||
|
ListAudit(ctx context.Context, request ListAuditRequestObject) (ListAuditResponseObject, error)
|
||||||
|
// Пользователи (admin, moderator)
|
||||||
|
// (GET /api/v1/admin/users)
|
||||||
|
ListUsers(ctx context.Context, request ListUsersRequestObject) (ListUsersResponseObject, error)
|
||||||
|
// Создать пользователя
|
||||||
|
// (POST /api/v1/admin/users)
|
||||||
|
CreateUser(ctx context.Context, request CreateUserRequestObject) (CreateUserResponseObject, error)
|
||||||
|
// Удалить пользователя (admin)
|
||||||
|
// (DELETE /api/v1/admin/users/{id})
|
||||||
|
DeleteUser(ctx context.Context, request DeleteUserRequestObject) (DeleteUserResponseObject, error)
|
||||||
|
// Изменить имя, роль, блокировку
|
||||||
|
// (PATCH /api/v1/admin/users/{id})
|
||||||
|
UpdateUser(ctx context.Context, request UpdateUserRequestObject) (UpdateUserResponseObject, error)
|
||||||
|
// Сбросить пароль
|
||||||
|
// (POST /api/v1/admin/users/{id}/reset-password)
|
||||||
|
ResetUserPassword(ctx context.Context, request ResetUserPasswordRequestObject) (ResetUserPasswordResponseObject, error)
|
||||||
// Вход по логину и паролю
|
// Вход по логину и паролю
|
||||||
// (POST /api/v1/auth/login)
|
// (POST /api/v1/auth/login)
|
||||||
Login(ctx context.Context, request LoginRequestObject) (LoginResponseObject, error)
|
Login(ctx context.Context, request LoginRequestObject) (LoginResponseObject, error)
|
||||||
@@ -1247,6 +1827,174 @@ type strictHandler struct {
|
|||||||
options StrictHTTPServerOptions
|
options StrictHTTPServerOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAudit operation middleware
|
||||||
|
func (sh *strictHandler) ListAudit(w http.ResponseWriter, r *http.Request, params ListAuditParams) {
|
||||||
|
var request ListAuditRequestObject
|
||||||
|
|
||||||
|
request.Params = params
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.ListAudit(ctx, request.(ListAuditRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "ListAudit")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(r.Context(), w, r, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
} else if validResponse, ok := response.(ListAuditResponseObject); ok {
|
||||||
|
if err := validResponse.VisitListAuditResponse(w); err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListUsers operation middleware
|
||||||
|
func (sh *strictHandler) ListUsers(w http.ResponseWriter, r *http.Request, params ListUsersParams) {
|
||||||
|
var request ListUsersRequestObject
|
||||||
|
|
||||||
|
request.Params = params
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.ListUsers(ctx, request.(ListUsersRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "ListUsers")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(r.Context(), w, r, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
} else if validResponse, ok := response.(ListUsersResponseObject); ok {
|
||||||
|
if err := validResponse.VisitListUsersResponse(w); err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser operation middleware
|
||||||
|
func (sh *strictHandler) CreateUser(w http.ResponseWriter, r *http.Request) {
|
||||||
|
var request CreateUserRequestObject
|
||||||
|
|
||||||
|
var body CreateUserJSONRequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = &body
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.CreateUser(ctx, request.(CreateUserRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "CreateUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(r.Context(), w, r, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
} else if validResponse, ok := response.(CreateUserResponseObject); ok {
|
||||||
|
if err := validResponse.VisitCreateUserResponse(w); err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser operation middleware
|
||||||
|
func (sh *strictHandler) DeleteUser(w http.ResponseWriter, r *http.Request, id UserId) {
|
||||||
|
var request DeleteUserRequestObject
|
||||||
|
|
||||||
|
request.Id = id
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.DeleteUser(ctx, request.(DeleteUserRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "DeleteUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(r.Context(), w, r, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
} else if validResponse, ok := response.(DeleteUserResponseObject); ok {
|
||||||
|
if err := validResponse.VisitDeleteUserResponse(w); err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser operation middleware
|
||||||
|
func (sh *strictHandler) UpdateUser(w http.ResponseWriter, r *http.Request, id UserId) {
|
||||||
|
var request UpdateUserRequestObject
|
||||||
|
|
||||||
|
request.Id = id
|
||||||
|
|
||||||
|
var body UpdateUserJSONRequestBody
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&body); err != nil {
|
||||||
|
sh.options.RequestErrorHandlerFunc(w, r, fmt.Errorf("can't decode JSON body: %w", err))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
request.Body = &body
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.UpdateUser(ctx, request.(UpdateUserRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "UpdateUser")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(r.Context(), w, r, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
} else if validResponse, ok := response.(UpdateUserResponseObject); ok {
|
||||||
|
if err := validResponse.VisitUpdateUserResponse(w); err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword operation middleware
|
||||||
|
func (sh *strictHandler) ResetUserPassword(w http.ResponseWriter, r *http.Request, id UserId) {
|
||||||
|
var request ResetUserPasswordRequestObject
|
||||||
|
|
||||||
|
request.Id = id
|
||||||
|
|
||||||
|
handler := func(ctx context.Context, w http.ResponseWriter, r *http.Request, request interface{}) (interface{}, error) {
|
||||||
|
return sh.ssi.ResetUserPassword(ctx, request.(ResetUserPasswordRequestObject))
|
||||||
|
}
|
||||||
|
for _, middleware := range sh.middlewares {
|
||||||
|
handler = middleware(handler, "ResetUserPassword")
|
||||||
|
}
|
||||||
|
|
||||||
|
response, err := handler(r.Context(), w, r, request)
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
} else if validResponse, ok := response.(ResetUserPasswordResponseObject); ok {
|
||||||
|
if err := validResponse.VisitResetUserPasswordResponse(w); err != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, err)
|
||||||
|
}
|
||||||
|
} else if response != nil {
|
||||||
|
sh.options.ResponseErrorHandlerFunc(w, r, fmt.Errorf("unexpected response type: %T", response))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Login operation middleware
|
// Login operation middleware
|
||||||
func (sh *strictHandler) Login(w http.ResponseWriter, r *http.Request) {
|
func (sh *strictHandler) Login(w http.ResponseWriter, r *http.Request) {
|
||||||
var request LoginRequestObject
|
var request LoginRequestObject
|
||||||
@@ -1601,64 +2349,82 @@ func (sh *strictHandler) GetReadyz(w http.ResponseWriter, r *http.Request) {
|
|||||||
// Base64 encoded, gzipped, json marshaled Swagger object
|
// Base64 encoded, gzipped, json marshaled Swagger object
|
||||||
var swaggerSpec = []string{
|
var swaggerSpec = []string{
|
||||||
|
|
||||||
"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",
|
||||||
|
"f8HaVub3Bs19it3CrZWt/wkAAP//LpP8775MAAA=",
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetSwagger returns the content of the embedded swagger specification file
|
// GetSwagger returns the content of the embedded swagger specification file
|
||||||
|
|||||||
@@ -0,0 +1,341 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
|
"strings"
|
||||||
|
"unicode/utf8"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
|
||||||
|
"pipu/api/internal/platform/apperr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Коды ошибок админки пользователей.
|
||||||
|
const (
|
||||||
|
CodeForbidden = "auth.forbidden"
|
||||||
|
CodeUserNotFound = "users.not_found"
|
||||||
|
CodeLastAdmin = "users.last_admin"
|
||||||
|
CodeRoleInvalid = "validation.role_invalid"
|
||||||
|
CodeCursorBad = "validation.cursor_invalid"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Длина временного пароля (создание пользователя, сброс).
|
||||||
|
const tempPasswordLen = 12
|
||||||
|
|
||||||
|
// Пределы размера страницы.
|
||||||
|
const (
|
||||||
|
defaultPageSize = 50
|
||||||
|
maxPageSize = 200
|
||||||
|
)
|
||||||
|
|
||||||
|
func forbidden() error { return apperr.Forbidden(CodeForbidden, "not allowed") }
|
||||||
|
|
||||||
|
func pageSize(limit *int) int {
|
||||||
|
if limit == nil || *limit <= 0 {
|
||||||
|
return defaultPageSize
|
||||||
|
}
|
||||||
|
return min(*limit, maxPageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
func actorOf(p Principal) Actor { return Actor{ID: p.UserID, Role: p.Role} }
|
||||||
|
|
||||||
|
func actorOfUser(u User) Actor { return Actor{ID: u.ID, Role: u.Role} }
|
||||||
|
|
||||||
|
// ListUsers — страница пользователей для админки (admin, moderator).
|
||||||
|
func (s *Service) ListUsers(ctx context.Context, p Principal, query string, role *Role, cursor string, limit *int,
|
||||||
|
) ([]User, string, error) {
|
||||||
|
if !Can(p.Role, PermListUsers) {
|
||||||
|
return nil, "", forbidden()
|
||||||
|
}
|
||||||
|
if role != nil && !role.Valid() {
|
||||||
|
return nil, "", apperr.Validation("role", CodeRoleInvalid, "unknown role", nil)
|
||||||
|
}
|
||||||
|
after := ""
|
||||||
|
if cursor != "" {
|
||||||
|
raw, err := base64.RawURLEncoding.DecodeString(cursor)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", apperr.Validation("cursor", CodeCursorBad, "malformed cursor", nil)
|
||||||
|
}
|
||||||
|
after = string(raw)
|
||||||
|
}
|
||||||
|
n := pageSize(limit)
|
||||||
|
users, err := s.store.ListUsers(ctx, UserFilter{Query: strings.TrimSpace(query), Role: role, After: after, Limit: n + 1})
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
next := ""
|
||||||
|
if len(users) > n {
|
||||||
|
users = users[:n]
|
||||||
|
next = base64.RawURLEncoding.EncodeToString([]byte(users[n-1].Username))
|
||||||
|
}
|
||||||
|
return users, next, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUserInput — форма создания пользователя.
|
||||||
|
type CreateUserInput struct {
|
||||||
|
Username string
|
||||||
|
DisplayName string
|
||||||
|
Role Role
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser создаёт пользователя с временным паролем, который возвращается один раз.
|
||||||
|
func (s *Service) CreateUser(ctx context.Context, p Principal, in CreateUserInput) (User, string, error) {
|
||||||
|
if !in.Role.Valid() {
|
||||||
|
return User{}, "", apperr.Validation("role", CodeRoleInvalid, "unknown role", nil)
|
||||||
|
}
|
||||||
|
if !CanAssignRole(p.Role, in.Role) || !CanManageUser(actorOf(p), Actor{Role: in.Role}, UserCreate) {
|
||||||
|
return User{}, "", forbidden()
|
||||||
|
}
|
||||||
|
username := NormalizeUsername(in.Username)
|
||||||
|
if err := ValidateUsername("username", username); err != nil {
|
||||||
|
return User{}, "", err
|
||||||
|
}
|
||||||
|
name, err := displayName(in.DisplayName)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, "", err
|
||||||
|
}
|
||||||
|
password, err := GeneratePassword(tempPasswordLen)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, "", err
|
||||||
|
}
|
||||||
|
hash, err := s.hasher.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
return User{}, "", err
|
||||||
|
}
|
||||||
|
locale := "ru"
|
||||||
|
if len(s.cfg.SupportedLocales) > 0 {
|
||||||
|
locale = s.cfg.SupportedLocales[0]
|
||||||
|
}
|
||||||
|
u := User{
|
||||||
|
ID: s.ids.New(), Username: username, DisplayName: name, PasswordHash: hash, Role: in.Role, Locale: locale,
|
||||||
|
MustChangePassword: true, CreatedAt: s.clock.Now(),
|
||||||
|
}
|
||||||
|
err = s.store.WithTx(ctx, func(tx Store) error {
|
||||||
|
if err := tx.CreateUser(ctx, u, &p.UserID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
|
||||||
|
ActorID: &p.UserID, Action: "user.create", TargetType: "user", TargetID: &u.ID,
|
||||||
|
Payload: map[string]any{"username": username, "role": string(in.Role)},
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if errors.Is(err, ErrUsernameTaken) {
|
||||||
|
return User{}, "", apperr.Validation("username", CodeUsernameTaken, "username is taken", nil)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return User{}, "", err
|
||||||
|
}
|
||||||
|
return u, password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// displayName обрезает пробелы и проверяет длину; пустое имя — nil.
|
||||||
|
func displayName(s string) (*string, error) {
|
||||||
|
s = strings.TrimSpace(s)
|
||||||
|
if utf8.RuneCountInString(s) > MaxDisplayName {
|
||||||
|
return nil, apperr.Validation("display_name", CodeDisplayNameTooLong, "display name too long",
|
||||||
|
map[string]any{"max": MaxDisplayName})
|
||||||
|
}
|
||||||
|
if s == "" {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return &s, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserPatch — изменения пользователя из админки. nil — не менять.
|
||||||
|
type UserPatch struct {
|
||||||
|
DisplayName *string
|
||||||
|
Role *Role
|
||||||
|
IsBlocked *bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser меняет имя, роль и блокировку с проверкой прав по каждому изменению.
|
||||||
|
func (s *Service) UpdateUser(ctx context.Context, p Principal, id uuid.UUID, patch UserPatch) (User, error) {
|
||||||
|
if patch.Role != nil && !patch.Role.Valid() {
|
||||||
|
return User{}, apperr.Validation("role", CodeRoleInvalid, "unknown role", nil)
|
||||||
|
}
|
||||||
|
var out User
|
||||||
|
err := s.store.WithTx(ctx, func(tx Store) error {
|
||||||
|
target, err := s.target(ctx, tx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
actor, tgt := actorOf(p), actorOfUser(target)
|
||||||
|
var (
|
||||||
|
upd UserUpdate
|
||||||
|
audits []AuditEntry
|
||||||
|
)
|
||||||
|
audit := func(action string, payload map[string]any) {
|
||||||
|
audits = append(audits, AuditEntry{
|
||||||
|
ActorID: &p.UserID, Action: action, TargetType: "user", TargetID: &target.ID, Payload: payload,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
if patch.DisplayName != nil {
|
||||||
|
if !CanManageUser(actor, tgt, UserEdit) {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
name, err := displayName(*patch.DisplayName)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
upd.DisplayName = &name
|
||||||
|
audit("user.rename", map[string]any{"display_name": name})
|
||||||
|
}
|
||||||
|
roleChange := patch.Role != nil && *patch.Role != target.Role
|
||||||
|
if roleChange {
|
||||||
|
if !CanManageUser(actor, tgt, UserChangeRole) || !CanAssignRole(p.Role, *patch.Role) {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
upd.Role, upd.RevokeTokens = patch.Role, true
|
||||||
|
audit("user.role_change", map[string]any{"from": string(target.Role), "to": string(*patch.Role)})
|
||||||
|
}
|
||||||
|
blockChange := patch.IsBlocked != nil && *patch.IsBlocked != target.IsBlocked
|
||||||
|
if blockChange {
|
||||||
|
if !CanManageUser(actor, tgt, UserBlock) {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
upd.IsBlocked, upd.RevokeTokens = patch.IsBlocked, true
|
||||||
|
if *patch.IsBlocked {
|
||||||
|
audit("user.block", nil)
|
||||||
|
} else {
|
||||||
|
audit("user.unblock", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(audits) == 0 {
|
||||||
|
// Пустое изменение: отвечаем текущим состоянием, но только тем, кому оно доступно.
|
||||||
|
if !CanManageUser(actor, tgt, UserEdit) {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
out = target
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
losesAdmin := target.Role == RoleAdmin && !target.IsBlocked &&
|
||||||
|
((roleChange && *patch.Role != RoleAdmin) || (blockChange && *patch.IsBlocked))
|
||||||
|
if losesAdmin {
|
||||||
|
if err := s.ensureNotLastAdmin(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if out, err = tx.UpdateUser(ctx, target.ID, upd); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if blockChange && *patch.IsBlocked {
|
||||||
|
if err := tx.RevokeUserSessions(ctx, target.ID, s.clock.Now()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, a := range audits {
|
||||||
|
if err := tx.InsertAudit(ctx, s.ids.New(), a); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
return out, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) target(ctx context.Context, st Store, id uuid.UUID) (User, error) {
|
||||||
|
u, err := st.GetUserByID(ctx, id)
|
||||||
|
if errors.Is(err, ErrNotFound) {
|
||||||
|
return User{}, apperr.NotFound(CodeUserNotFound, "user not found")
|
||||||
|
}
|
||||||
|
return u, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Service) ensureNotLastAdmin(ctx context.Context, tx Store) error {
|
||||||
|
n, err := tx.LockActiveAdmins(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if n <= 1 {
|
||||||
|
return apperr.Conflict(CodeLastAdmin, "the last active admin cannot be demoted, blocked or deleted")
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword выдаёт новый временный пароль и завершает все сессии пользователя.
|
||||||
|
func (s *Service) ResetPassword(ctx context.Context, p Principal, id uuid.UUID) (string, error) {
|
||||||
|
password, err := GeneratePassword(tempPasswordLen)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
hash, err := s.hasher.Hash(password)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
err = s.store.WithTx(ctx, func(tx Store) error {
|
||||||
|
target, err := s.target(ctx, tx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !CanManageUser(actorOf(p), actorOfUser(target), UserResetPassword) {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
if err := tx.ResetPassword(ctx, target.ID, hash); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := tx.RevokeUserSessions(ctx, target.ID, s.clock.Now()); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
|
||||||
|
ActorID: &p.UserID, Action: "user.password_reset", TargetType: "user", TargetID: &target.ID,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return password, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser удаляет пользователя (только админ, не себя и не последнего админа).
|
||||||
|
func (s *Service) DeleteUser(ctx context.Context, p Principal, id uuid.UUID) error {
|
||||||
|
return s.store.WithTx(ctx, func(tx Store) error {
|
||||||
|
target, err := s.target(ctx, tx, id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if !CanManageUser(actorOf(p), actorOfUser(target), UserDelete) {
|
||||||
|
return forbidden()
|
||||||
|
}
|
||||||
|
if target.Role == RoleAdmin && !target.IsBlocked {
|
||||||
|
if err := s.ensureNotLastAdmin(ctx, tx); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := tx.InsertAudit(ctx, s.ids.New(), AuditEntry{
|
||||||
|
ActorID: &p.UserID, Action: "user.delete", TargetType: "user", TargetID: &target.ID,
|
||||||
|
Payload: map[string]any{"username": target.Username, "role": string(target.Role)},
|
||||||
|
}); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.DeleteUser(ctx, target.ID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudit — страница журнала действий (только админ).
|
||||||
|
func (s *Service) ListAudit(ctx context.Context, p Principal, cursor string, limit *int) ([]AuditRecord, string, error) {
|
||||||
|
if !Can(p.Role, PermSystemStatus) {
|
||||||
|
return nil, "", forbidden()
|
||||||
|
}
|
||||||
|
before := uuid.Nil
|
||||||
|
if cursor != "" {
|
||||||
|
var err error
|
||||||
|
if before, err = uuid.Parse(cursor); err != nil {
|
||||||
|
return nil, "", apperr.Validation("cursor", CodeCursorBad, "malformed cursor", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
n := pageSize(limit)
|
||||||
|
records, err := s.store.ListAudit(ctx, before, n+1)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
next := ""
|
||||||
|
if len(records) > n {
|
||||||
|
records = records[:n]
|
||||||
|
next = records[n-1].ID.String()
|
||||||
|
}
|
||||||
|
return records, next, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,152 @@
|
|||||||
|
package auth
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"pipu/api/internal/api/gen"
|
||||||
|
)
|
||||||
|
|
||||||
|
// ListUsers реализует gen.StrictServerInterface.
|
||||||
|
func (h *Handler) ListUsers(ctx context.Context, req gen.ListUsersRequestObject) (gen.ListUsersResponseObject, error) {
|
||||||
|
p, err := principal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var (
|
||||||
|
q, cursor string
|
||||||
|
role *Role
|
||||||
|
)
|
||||||
|
if req.Params.Q != nil {
|
||||||
|
q = *req.Params.Q
|
||||||
|
}
|
||||||
|
if req.Params.Cursor != nil {
|
||||||
|
cursor = *req.Params.Cursor
|
||||||
|
}
|
||||||
|
if req.Params.Role != nil {
|
||||||
|
r := Role(*req.Params.Role)
|
||||||
|
role = &r
|
||||||
|
}
|
||||||
|
users, next, err := h.svc.ListUsers(ctx, p, q, role, cursor, req.Params.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]gen.AdminUser, 0, len(users))
|
||||||
|
for _, u := range users {
|
||||||
|
items = append(items, AdminUserDTO(u))
|
||||||
|
}
|
||||||
|
return gen.ListUsers200JSONResponse{Items: items, NextCursor: optional(next)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// CreateUser реализует gen.StrictServerInterface.
|
||||||
|
func (h *Handler) CreateUser(ctx context.Context, req gen.CreateUserRequestObject) (gen.CreateUserResponseObject, error) {
|
||||||
|
p, err := principal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
in := CreateUserInput{Username: req.Body.Username, Role: Role(req.Body.Role)}
|
||||||
|
if req.Body.DisplayName != nil {
|
||||||
|
in.DisplayName = *req.Body.DisplayName
|
||||||
|
}
|
||||||
|
u, password, err := h.svc.CreateUser(ctx, p, in)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return gen.CreateUser201JSONResponse{User: AdminUserDTO(u), TemporaryPassword: password}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser реализует gen.StrictServerInterface.
|
||||||
|
func (h *Handler) UpdateUser(ctx context.Context, req gen.UpdateUserRequestObject) (gen.UpdateUserResponseObject, error) {
|
||||||
|
p, err := principal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
patch := UserPatch{DisplayName: req.Body.DisplayName, IsBlocked: req.Body.IsBlocked}
|
||||||
|
if req.Body.Role != nil {
|
||||||
|
r := Role(*req.Body.Role)
|
||||||
|
patch.Role = &r
|
||||||
|
}
|
||||||
|
u, err := h.svc.UpdateUser(ctx, p, req.Id, patch)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return gen.UpdateUser200JSONResponse(AdminUserDTO(u)), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser реализует gen.StrictServerInterface.
|
||||||
|
func (h *Handler) DeleteUser(ctx context.Context, req gen.DeleteUserRequestObject) (gen.DeleteUserResponseObject, error) {
|
||||||
|
p, err := principal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if err := h.svc.DeleteUser(ctx, p, req.Id); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return gen.DeleteUser204Response{}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetUserPassword реализует gen.StrictServerInterface.
|
||||||
|
func (h *Handler) ResetUserPassword(ctx context.Context, req gen.ResetUserPasswordRequestObject) (gen.ResetUserPasswordResponseObject, error) {
|
||||||
|
p, err := principal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
password, err := h.svc.ResetPassword(ctx, p, req.Id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return gen.ResetUserPassword200JSONResponse{TemporaryPassword: password}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudit реализует gen.StrictServerInterface.
|
||||||
|
func (h *Handler) ListAudit(ctx context.Context, req gen.ListAuditRequestObject) (gen.ListAuditResponseObject, error) {
|
||||||
|
p, err := principal(ctx)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
cursor := ""
|
||||||
|
if req.Params.Cursor != nil {
|
||||||
|
cursor = *req.Params.Cursor
|
||||||
|
}
|
||||||
|
records, next, err := h.svc.ListAudit(ctx, p, cursor, req.Params.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
items := make([]gen.AuditEntry, 0, len(records))
|
||||||
|
for _, r := range records {
|
||||||
|
e := gen.AuditEntry{
|
||||||
|
Id: r.ID, Action: r.Action, TargetType: r.TargetType, TargetId: r.TargetID, CreatedAt: r.CreatedAt,
|
||||||
|
}
|
||||||
|
if r.ActorID != nil && r.ActorUsername != nil {
|
||||||
|
e.Actor = &gen.AuditActor{Id: *r.ActorID, Username: *r.ActorUsername}
|
||||||
|
}
|
||||||
|
if r.Payload != nil {
|
||||||
|
payload := r.Payload
|
||||||
|
e.Payload = &payload
|
||||||
|
}
|
||||||
|
items = append(items, e)
|
||||||
|
}
|
||||||
|
return gen.ListAudit200JSONResponse{Items: items, NextCursor: optional(next)}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminUserDTO переводит пользователя в схему AdminUser.
|
||||||
|
func AdminUserDTO(u User) gen.AdminUser {
|
||||||
|
return gen.AdminUser{
|
||||||
|
Id: u.ID,
|
||||||
|
Username: u.Username,
|
||||||
|
DisplayName: u.DisplayName,
|
||||||
|
Role: gen.Role(u.Role),
|
||||||
|
Locale: u.Locale,
|
||||||
|
IsBlocked: u.IsBlocked,
|
||||||
|
MustChangePassword: u.MustChangePassword,
|
||||||
|
MustChangeCredentials: u.MustChangeCredentials,
|
||||||
|
CreatedAt: u.CreatedAt,
|
||||||
|
LastLoginAt: u.LastLoginAt,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func optional(s string) *string {
|
||||||
|
if s == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
@@ -0,0 +1,379 @@
|
|||||||
|
package auth_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"net/url"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
"github.com/google/uuid"
|
||||||
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
|
|
||||||
|
"pipu/api/internal/auth"
|
||||||
|
"pipu/api/internal/platform/apperr"
|
||||||
|
)
|
||||||
|
|
||||||
|
// signIn входит под пользователем, у которого может стоять must_change_password, и сразу меняет пароль.
|
||||||
|
func (e *env) signIn(username, password string) string {
|
||||||
|
e.t.Helper()
|
||||||
|
r := e.mustLogin(username, password, "web")
|
||||||
|
if user(r)["must_change_password"] == true {
|
||||||
|
r = e.do(http.MethodPost, "/api/v1/me/password",
|
||||||
|
map[string]any{"new_password": username + "-own-password"}, bearer(access(r)))
|
||||||
|
require.Equal(e.t, http.StatusOK, r.Code, r.Body.String())
|
||||||
|
}
|
||||||
|
return access(r)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (e *env) createUser(token, username, role string) resp {
|
||||||
|
e.t.Helper()
|
||||||
|
return e.do(http.MethodPost, "/api/v1/admin/users",
|
||||||
|
map[string]any{"username": username, "role": role, "display_name": " " + username + " "}, bearer(token))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сценарий 2 из docs/testing.md: админ создаёт модератора, модератор — пользователя,
|
||||||
|
// но не модератора.
|
||||||
|
func TestAdminCreatesModeratorWhoCreatesUser(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
|
||||||
|
created := e.createUser(admin, "Mod.One", "moderator")
|
||||||
|
require.Equal(t, http.StatusCreated, created.Code, created.Body.String())
|
||||||
|
modUser := created.body["user"].(map[string]any)
|
||||||
|
assert.Equal(t, "mod.one", modUser["username"])
|
||||||
|
assert.Equal(t, "Mod.One", modUser["display_name"])
|
||||||
|
assert.Equal(t, true, modUser["must_change_password"])
|
||||||
|
temp := created.body["temporary_password"].(string)
|
||||||
|
assert.Len(t, temp, 12)
|
||||||
|
|
||||||
|
first := e.mustLogin("mod.one", temp, "web")
|
||||||
|
assert.Equal(t, auth.CodePasswordChangeRequired,
|
||||||
|
e.do(http.MethodGet, "/api/v1/admin/users", nil, bearer(access(first))).code())
|
||||||
|
mod := e.signIn("mod.one", temp)
|
||||||
|
|
||||||
|
listenerUser := e.createUser(mod, "listener", "user")
|
||||||
|
require.Equal(t, http.StatusCreated, listenerUser.Code)
|
||||||
|
for _, role := range []string{"moderator", "admin"} {
|
||||||
|
r := e.createUser(mod, "sneaky-"+role, role)
|
||||||
|
assert.Equal(t, http.StatusForbidden, r.Code, role)
|
||||||
|
assert.Equal(t, auth.CodeForbidden, r.code())
|
||||||
|
}
|
||||||
|
|
||||||
|
list := e.do(http.MethodGet, "/api/v1/admin/users", nil, bearer(mod))
|
||||||
|
require.Equal(t, http.StatusOK, list.Code)
|
||||||
|
var names []string
|
||||||
|
for _, it := range list.body["items"].([]any) {
|
||||||
|
names = append(names, it.(map[string]any)["username"].(string))
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{"listener", "mod.one", "root"}, names)
|
||||||
|
assert.Nil(t, list.body["next_cursor"])
|
||||||
|
|
||||||
|
listener := e.signIn("listener", listenerUser.body["temporary_password"].(string))
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/users", nil, bearer(listener)).code())
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.createUser(listener, "another", "user").code())
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCreateUserValidation(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
|
||||||
|
assert.Equal(t, auth.CodeUsernameTaken, e.createUser(admin, "ROOT", "user").code())
|
||||||
|
assert.Equal(t, auth.CodeUsernameInvalid, e.createUser(admin, "no spaces", "user").code())
|
||||||
|
assert.Equal(t, auth.CodeRoleInvalid, e.createUser(admin, "valid", "root").code())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Матрица «кто кого может» через API: сброс пароля, блокировка, смена роли, удаление.
|
||||||
|
func TestManageUsersMatrix(t *testing.T) {
|
||||||
|
type actorCase struct {
|
||||||
|
role auth.Role
|
||||||
|
// разрешено ли действие над целью с ролью user / moderator / admin
|
||||||
|
reset, block, role2, del [3]bool
|
||||||
|
}
|
||||||
|
cases := []actorCase{
|
||||||
|
{auth.RoleModerator, [3]bool{true, false, false}, [3]bool{true, false, false}, [3]bool{false, false, false}, [3]bool{false, false, false}},
|
||||||
|
{auth.RoleAdmin, [3]bool{true, true, true}, [3]bool{true, true, true}, [3]bool{true, true, true}, [3]bool{true, true, true}},
|
||||||
|
}
|
||||||
|
targets := [3]auth.Role{auth.RoleUser, auth.RoleModerator, auth.RoleAdmin}
|
||||||
|
for _, ac := range cases {
|
||||||
|
for ti, tr := range targets {
|
||||||
|
actions := []struct {
|
||||||
|
name string
|
||||||
|
allowed bool
|
||||||
|
call func(e *env, tok, id string) resp
|
||||||
|
}{
|
||||||
|
{"reset", ac.reset[ti], func(e *env, tok, id string) resp {
|
||||||
|
return e.do(http.MethodPost, "/api/v1/admin/users/"+id+"/reset-password", nil, bearer(tok))
|
||||||
|
}},
|
||||||
|
{"block", ac.block[ti], func(e *env, tok, id string) resp {
|
||||||
|
return e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"is_blocked": true}, bearer(tok))
|
||||||
|
}},
|
||||||
|
{"role", ac.role2[ti], func(e *env, tok, id string) resp {
|
||||||
|
next := "user"
|
||||||
|
if tr == auth.RoleUser {
|
||||||
|
next = "moderator"
|
||||||
|
}
|
||||||
|
return e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"role": next}, bearer(tok))
|
||||||
|
}},
|
||||||
|
{"delete", ac.del[ti], func(e *env, tok, id string) resp {
|
||||||
|
return e.do(http.MethodDelete, "/api/v1/admin/users/"+id, nil, bearer(tok))
|
||||||
|
}},
|
||||||
|
}
|
||||||
|
for _, a := range actions {
|
||||||
|
t.Run(fmt.Sprintf("%s %s %s", ac.role, a.name, tr), func(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("actor", "actor-password-1", ac.role)
|
||||||
|
e.addUser("spare-admin", "spare-password-1", auth.RoleAdmin) // чтобы не упереться в «последнего админа»
|
||||||
|
target := e.addUser("target", "target-password", tr)
|
||||||
|
tok := e.signIn("actor", "actor-password-1")
|
||||||
|
r := a.call(e, tok, target.ID.String())
|
||||||
|
if a.allowed {
|
||||||
|
assert.Less(t, r.Code, 300, r.Body.String())
|
||||||
|
} else {
|
||||||
|
assert.Equal(t, http.StatusForbidden, r.Code, r.Body.String())
|
||||||
|
assert.Equal(t, auth.CodeForbidden, r.code())
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestModeratorCannotTouchSelfOrRoles(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
mod := e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
||||||
|
u := e.addUser("listener", "listener-pass", auth.RoleUser)
|
||||||
|
tok := e.signIn("mod", "moderator-pass-1")
|
||||||
|
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+mod.ID.String(),
|
||||||
|
map[string]any{"display_name": "Boss"}, bearer(tok)).code())
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+u.ID.String(),
|
||||||
|
map[string]any{"display_name": "ok", "role": "moderator"}, bearer(tok)).code(), "one forbidden change rejects the patch")
|
||||||
|
assert.Nil(t, e.store.Users[u.ID].DisplayName, "rejected patch changes nothing")
|
||||||
|
|
||||||
|
ok := e.do(http.MethodPatch, "/api/v1/admin/users/"+u.ID.String(),
|
||||||
|
map[string]any{"display_name": "Listener", "role": "user"}, bearer(tok))
|
||||||
|
require.Equal(t, http.StatusOK, ok.Code, ok.Body.String())
|
||||||
|
assert.Equal(t, "Listener", ok.body["display_name"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAdminCannotDemoteBlockOrDeleteSelf(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
root := e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
e.addUser("other", "other-password-1", auth.RoleAdmin)
|
||||||
|
tok := e.signIn("root", "root-password-1")
|
||||||
|
id := root.ID.String()
|
||||||
|
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"role": "user"}, bearer(tok)).code())
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"is_blocked": true}, bearer(tok)).code())
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodDelete, "/api/v1/admin/users/"+id, nil, bearer(tok)).code())
|
||||||
|
rename := e.do(http.MethodPatch, "/api/v1/admin/users/"+id, map[string]any{"display_name": "Root"}, bearer(tok))
|
||||||
|
assert.Equal(t, http.StatusOK, rename.Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestLastAdminIsProtected(t *testing.T) {
|
||||||
|
// Через API последнего админа не затронуть: себя менять нельзя, а действующий админ — второй.
|
||||||
|
// Остаётся гонка (админ удалён, пока его запрос в пути) — её и закрывает проверка в сервисе.
|
||||||
|
e := newEnv(t)
|
||||||
|
only := e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
ghost := auth.Principal{UserID: uuid.New(), Role: auth.RoleAdmin}
|
||||||
|
ctx := context.Background()
|
||||||
|
|
||||||
|
role := auth.RoleUser
|
||||||
|
blocked := true
|
||||||
|
for name, call := range map[string]func() error{
|
||||||
|
"demote": func() error { _, err := e.svc.UpdateUser(ctx, ghost, only.ID, auth.UserPatch{Role: &role}); return err },
|
||||||
|
"block": func() error {
|
||||||
|
_, err := e.svc.UpdateUser(ctx, ghost, only.ID, auth.UserPatch{IsBlocked: &blocked})
|
||||||
|
return err
|
||||||
|
},
|
||||||
|
"delete": func() error { return e.svc.DeleteUser(ctx, ghost, only.ID) },
|
||||||
|
} {
|
||||||
|
err := call()
|
||||||
|
var ae *apperr.Error
|
||||||
|
require.ErrorAs(t, err, &ae, name)
|
||||||
|
assert.Equal(t, auth.CodeLastAdmin, ae.Code, name)
|
||||||
|
assert.Equal(t, http.StatusConflict, ae.Status, name)
|
||||||
|
}
|
||||||
|
assert.Equal(t, auth.RoleAdmin, e.store.Users[only.ID].Role)
|
||||||
|
assert.False(t, e.store.Users[only.ID].IsBlocked)
|
||||||
|
assert.Empty(t, e.store.Audit, "rolled back changes leave no audit")
|
||||||
|
|
||||||
|
// Заблокированный админ не считается: второго активного нет.
|
||||||
|
e.addUser("sleeping", "sleeping-pass-1", auth.RoleAdmin, func(u *auth.User) { u.IsBlocked = true })
|
||||||
|
_, err := e.svc.UpdateUser(ctx, ghost, only.ID, auth.UserPatch{Role: &role})
|
||||||
|
assert.ErrorIs(t, err, apperr.Conflict(auth.CodeLastAdmin, ""))
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestBlockAndRoleChangeRevokeAccess(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
web := e.mustLogin("listener", "listener-pass", "web")
|
||||||
|
cookie := web.cookie(auth.RefreshCookie)
|
||||||
|
|
||||||
|
r := e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(), map[string]any{"role": "moderator"}, bearer(admin))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
||||||
|
assert.Equal(t, "moderator", r.body["role"])
|
||||||
|
assert.Equal(t, auth.CodeTokenRevoked, e.do(http.MethodGet, "/api/v1/me", nil, bearer(access(web))).code())
|
||||||
|
|
||||||
|
refreshed := e.do(http.MethodPost, "/api/v1/auth/refresh", nil, withCookie(cookie))
|
||||||
|
require.Equal(t, http.StatusOK, refreshed.Code, "role change keeps sessions")
|
||||||
|
assert.Equal(t, "moderator", user(refreshed)["role"])
|
||||||
|
tok := access(refreshed)
|
||||||
|
|
||||||
|
r = e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(), map[string]any{"is_blocked": true}, bearer(admin))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code)
|
||||||
|
assert.Equal(t, true, r.body["is_blocked"])
|
||||||
|
assert.Equal(t, http.StatusUnauthorized, e.do(http.MethodGet, "/api/v1/me", nil, bearer(tok)).Code)
|
||||||
|
assert.Equal(t, auth.CodeRefreshInvalid,
|
||||||
|
e.do(http.MethodPost, "/api/v1/auth/refresh", nil, withCookie(refreshed.cookie(auth.RefreshCookie))).code(),
|
||||||
|
"blocking ends sessions")
|
||||||
|
|
||||||
|
r = e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(), map[string]any{"is_blocked": false}, bearer(admin))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code)
|
||||||
|
e.mustLogin("listener", "listener-pass", "web")
|
||||||
|
|
||||||
|
var actions []string
|
||||||
|
for _, a := range e.store.Audit {
|
||||||
|
actions = append(actions, a.Action)
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{"user.role_change", "user.block", "user.unblock"}, actions)
|
||||||
|
assert.Equal(t, map[string]any{"from": "user", "to": "moderator"}, e.store.Audit[0].Payload)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestNoOpPatch(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
r := e.do(http.MethodPatch, "/api/v1/admin/users/"+target.ID.String(),
|
||||||
|
map[string]any{"role": "user", "is_blocked": false}, bearer(admin))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code)
|
||||||
|
assert.Empty(t, e.store.Audit)
|
||||||
|
assert.Zero(t, e.store.Users[target.ID].TokenVersion)
|
||||||
|
|
||||||
|
assert.Equal(t, auth.CodeUserNotFound, e.do(http.MethodPatch, "/api/v1/admin/users/"+uuid.NewString(),
|
||||||
|
map[string]any{"display_name": "x"}, bearer(admin)).code())
|
||||||
|
}
|
||||||
|
|
||||||
|
// Сценарий 10 из docs/testing.md: сброс пароля → обязательная смена при входе.
|
||||||
|
func TestResetPassword(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
||||||
|
target := e.addUser("listener", "listener-pass", auth.RoleUser, func(u *auth.User) { u.FailedLoginCount = 9 })
|
||||||
|
mod := e.signIn("mod", "moderator-pass-1")
|
||||||
|
phone := e.mustLogin("listener", "listener-pass", "android")
|
||||||
|
|
||||||
|
r := e.do(http.MethodPost, "/api/v1/admin/users/"+target.ID.String()+"/reset-password", nil, bearer(mod))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
||||||
|
temp := r.body["temporary_password"].(string)
|
||||||
|
|
||||||
|
assert.Equal(t, auth.CodeTokenRevoked, e.do(http.MethodGet, "/api/v1/me", nil, bearer(access(phone))).code())
|
||||||
|
assert.Equal(t, auth.CodeRefreshInvalid, e.do(http.MethodPost, "/api/v1/auth/refresh",
|
||||||
|
map[string]any{"refresh_token": phone.body["refresh_token"]}).code())
|
||||||
|
assert.Equal(t, auth.CodeInvalidCredentials, e.login("listener", "listener-pass", "web").code())
|
||||||
|
|
||||||
|
again := e.mustLogin("listener", temp, "web")
|
||||||
|
assert.Equal(t, true, user(again)["must_change_password"])
|
||||||
|
assert.Equal(t, auth.CodePasswordChangeRequired, e.do(http.MethodGet, "/api/v1/me/sessions", nil, bearer(access(again))).code())
|
||||||
|
assert.Equal(t, "user.password_reset", e.store.Audit[len(e.store.Audit)-1].Action)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeleteUser(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
target := e.addUser("listener", "listener-pass", auth.RoleUser)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
tok := access(e.mustLogin("listener", "listener-pass", "web"))
|
||||||
|
|
||||||
|
assert.Equal(t, http.StatusNoContent, e.do(http.MethodDelete, "/api/v1/admin/users/"+target.ID.String(), nil, bearer(admin)).Code)
|
||||||
|
assert.NotContains(t, e.store.Users, target.ID)
|
||||||
|
assert.Equal(t, auth.CodeTokenRevoked, e.do(http.MethodGet, "/api/v1/me", nil, bearer(tok)).code())
|
||||||
|
assert.Equal(t, auth.CodeUserNotFound, e.do(http.MethodDelete, "/api/v1/admin/users/"+target.ID.String(), nil, bearer(admin)).code())
|
||||||
|
last := e.store.Audit[len(e.store.Audit)-1]
|
||||||
|
assert.Equal(t, "user.delete", last.Action)
|
||||||
|
assert.Equal(t, "listener", last.Payload["username"])
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestListUsersFiltersAndPages(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
for _, n := range []string{"anna", "boris", "vera", "gleb", "dina"} {
|
||||||
|
e.addUser(n, n+"-password", auth.RoleUser)
|
||||||
|
}
|
||||||
|
name := "Анна Каренина"
|
||||||
|
e.addUser("kate", "kate-password", auth.RoleModerator, func(u *auth.User) { u.DisplayName = &name })
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
list := func(q url.Values) resp {
|
||||||
|
return e.do(http.MethodGet, "/api/v1/admin/users?"+q.Encode(), nil, bearer(admin))
|
||||||
|
}
|
||||||
|
usernames := func(r resp) []string {
|
||||||
|
var out []string
|
||||||
|
for _, it := range r.body["items"].([]any) {
|
||||||
|
out = append(out, it.(map[string]any)["username"].(string))
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
all []string
|
||||||
|
cursor string
|
||||||
|
)
|
||||||
|
for page := 0; ; page++ {
|
||||||
|
q := url.Values{"limit": {"3"}}
|
||||||
|
if cursor != "" {
|
||||||
|
q.Set("cursor", cursor)
|
||||||
|
}
|
||||||
|
r := list(q)
|
||||||
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
||||||
|
all = append(all, usernames(r)...)
|
||||||
|
next, _ := r.body["next_cursor"].(string)
|
||||||
|
if next == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
cursor = next
|
||||||
|
require.Less(t, page, 5)
|
||||||
|
}
|
||||||
|
assert.Equal(t, []string{"anna", "boris", "dina", "gleb", "kate", "root", "vera"}, all)
|
||||||
|
|
||||||
|
assert.Equal(t, []string{"kate"}, usernames(list(url.Values{"q": {"анна"}})), "matches display name, case-insensitive")
|
||||||
|
assert.Equal(t, []string{"anna"}, usernames(list(url.Values{"q": {"AN"}})))
|
||||||
|
assert.Equal(t, []string{"kate"}, usernames(list(url.Values{"role": {"moderator"}})))
|
||||||
|
assert.Equal(t, auth.CodeCursorBad, list(url.Values{"cursor": {"%%%"}}).code())
|
||||||
|
assert.Equal(t, http.StatusBadRequest, list(url.Values{"limit": {"lots"}}).Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestAudit(t *testing.T) {
|
||||||
|
e := newEnv(t)
|
||||||
|
e.addUser("root", "root-password-1", auth.RoleAdmin)
|
||||||
|
e.addUser("mod", "moderator-pass-1", auth.RoleModerator)
|
||||||
|
admin := e.signIn("root", "root-password-1")
|
||||||
|
for _, n := range []string{"u1", "u2", "u3"} {
|
||||||
|
require.Equal(t, http.StatusCreated, e.createUser(admin, n+"-user", "user").Code)
|
||||||
|
}
|
||||||
|
|
||||||
|
r := e.do(http.MethodGet, "/api/v1/admin/audit?limit=2", nil, bearer(admin))
|
||||||
|
require.Equal(t, http.StatusOK, r.Code, r.Body.String())
|
||||||
|
items := r.body["items"].([]any)
|
||||||
|
require.Len(t, items, 2)
|
||||||
|
newest := items[0].(map[string]any)
|
||||||
|
assert.Equal(t, "user.create", newest["action"])
|
||||||
|
assert.Equal(t, "u3-user", newest["payload"].(map[string]any)["username"])
|
||||||
|
assert.Equal(t, "root", newest["actor"].(map[string]any)["username"])
|
||||||
|
assert.Equal(t, "user", newest["target_type"])
|
||||||
|
|
||||||
|
next := r.body["next_cursor"].(string)
|
||||||
|
r2 := e.do(http.MethodGet, "/api/v1/admin/audit?limit=2&cursor="+next, nil, bearer(admin))
|
||||||
|
rest := r2.body["items"].([]any)
|
||||||
|
require.Len(t, rest, 1)
|
||||||
|
assert.Equal(t, "u1-user", rest[0].(map[string]any)["payload"].(map[string]any)["username"])
|
||||||
|
assert.Nil(t, r2.body["next_cursor"])
|
||||||
|
|
||||||
|
mod := e.signIn("mod", "moderator-pass-1")
|
||||||
|
assert.Equal(t, auth.CodeForbidden, e.do(http.MethodGet, "/api/v1/admin/audit", nil, bearer(mod)).code())
|
||||||
|
}
|
||||||
@@ -23,6 +23,7 @@ type MemStore struct {
|
|||||||
Users map[uuid.UUID]auth.User
|
Users map[uuid.UUID]auth.User
|
||||||
Tokens map[uuid.UUID]auth.RefreshToken
|
Tokens map[uuid.UUID]auth.RefreshToken
|
||||||
Audit []auth.AuditEntry
|
Audit []auth.AuditEntry
|
||||||
|
AuditIDs []uuid.UUID
|
||||||
Settings map[string]any
|
Settings map[string]any
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,13 +49,16 @@ func (s *MemStore) lock() func() {
|
|||||||
func (s *MemStore) WithTx(_ context.Context, fn func(auth.Store) error) error {
|
func (s *MemStore) WithTx(_ context.Context, fn func(auth.Store) error) error {
|
||||||
unlock := s.lock()
|
unlock := s.lock()
|
||||||
defer unlock()
|
defer unlock()
|
||||||
users, tokens, audit, settings := maps.Clone(s.Users), maps.Clone(s.Tokens), slices.Clone(s.Audit), maps.Clone(s.Settings)
|
users, tokens, settings := maps.Clone(s.Users), maps.Clone(s.Tokens), maps.Clone(s.Settings)
|
||||||
tx := &MemStore{mu: s.mu, inTx: true, Users: s.Users, Tokens: s.Tokens, Audit: s.Audit, Settings: s.Settings}
|
audit, auditIDs := slices.Clone(s.Audit), slices.Clone(s.AuditIDs)
|
||||||
|
tx := &MemStore{
|
||||||
|
mu: s.mu, inTx: true, Users: s.Users, Tokens: s.Tokens, Audit: s.Audit, AuditIDs: s.AuditIDs, Settings: s.Settings,
|
||||||
|
}
|
||||||
if err := fn(tx); err != nil {
|
if err := fn(tx); err != nil {
|
||||||
s.Users, s.Tokens, s.Audit, s.Settings = users, tokens, audit, settings
|
s.Users, s.Tokens, s.Audit, s.AuditIDs, s.Settings = users, tokens, audit, auditIDs, settings
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
s.Users, s.Tokens, s.Audit, s.Settings = tx.Users, tx.Tokens, tx.Audit, tx.Settings
|
s.Users, s.Tokens, s.Audit, s.AuditIDs, s.Settings = tx.Users, tx.Tokens, tx.Audit, tx.AuditIDs, tx.Settings
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,13 +255,120 @@ func (s *MemStore) ListSessions(_ context.Context, userID uuid.UUID, now time.Ti
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// InsertAudit реализует auth.Store.
|
// ListUsers реализует auth.Store.
|
||||||
func (s *MemStore) InsertAudit(_ context.Context, _ uuid.UUID, e auth.AuditEntry) error {
|
func (s *MemStore) ListUsers(_ context.Context, f auth.UserFilter) ([]auth.User, error) {
|
||||||
|
defer s.lock()()
|
||||||
|
q := strings.ToLower(f.Query)
|
||||||
|
var out []auth.User
|
||||||
|
for _, u := range s.Users {
|
||||||
|
name := ""
|
||||||
|
if u.DisplayName != nil {
|
||||||
|
name = strings.ToLower(*u.DisplayName)
|
||||||
|
}
|
||||||
|
if q != "" && !strings.Contains(strings.ToLower(u.Username), q) && !strings.Contains(name, q) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.Role != nil && u.Role != *f.Role {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if f.After != "" && strings.ToLower(u.Username) <= strings.ToLower(f.After) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
out = append(out, cloneUser(u))
|
||||||
|
}
|
||||||
|
slices.SortFunc(out, func(a, b auth.User) int {
|
||||||
|
return strings.Compare(strings.ToLower(a.Username), strings.ToLower(b.Username))
|
||||||
|
})
|
||||||
|
if len(out) > f.Limit {
|
||||||
|
out = out[:f.Limit]
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockActiveAdmins реализует auth.Store.
|
||||||
|
func (s *MemStore) LockActiveAdmins(ctx context.Context) (int, error) { return s.CountAdmins(ctx) }
|
||||||
|
|
||||||
|
// UpdateUser реализует auth.Store.
|
||||||
|
func (s *MemStore) UpdateUser(ctx context.Context, id uuid.UUID, upd auth.UserUpdate) (auth.User, error) {
|
||||||
|
err := s.update(id, func(u *auth.User) {
|
||||||
|
if upd.DisplayName != nil {
|
||||||
|
u.DisplayName = *upd.DisplayName
|
||||||
|
}
|
||||||
|
if upd.Role != nil {
|
||||||
|
u.Role = *upd.Role
|
||||||
|
}
|
||||||
|
if upd.IsBlocked != nil {
|
||||||
|
u.IsBlocked = *upd.IsBlocked
|
||||||
|
}
|
||||||
|
if upd.RevokeTokens {
|
||||||
|
u.TokenVersion++
|
||||||
|
}
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return auth.User{}, err
|
||||||
|
}
|
||||||
|
return s.GetUserByID(ctx, id)
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword реализует auth.Store.
|
||||||
|
func (s *MemStore) ResetPassword(_ context.Context, id uuid.UUID, hash string) error {
|
||||||
|
return s.update(id, func(u *auth.User) {
|
||||||
|
u.PasswordHash, u.MustChangePassword = hash, true
|
||||||
|
u.FailedLoginCount, u.LockedUntil = 0, nil
|
||||||
|
u.TokenVersion++
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser реализует auth.Store: токены удаляются, ссылки в журнале обнуляются, как в PostgreSQL.
|
||||||
|
func (s *MemStore) DeleteUser(_ context.Context, id uuid.UUID) error {
|
||||||
|
defer s.lock()()
|
||||||
|
delete(s.Users, id)
|
||||||
|
for tid, t := range s.Tokens {
|
||||||
|
if t.UserID == id {
|
||||||
|
delete(s.Tokens, tid)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i := range s.Audit {
|
||||||
|
if s.Audit[i].ActorID != nil && *s.Audit[i].ActorID == id {
|
||||||
|
s.Audit[i].ActorID = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// InsertAudit реализует auth.Store. ID сохраняется в AuditIDs параллельно Audit.
|
||||||
|
func (s *MemStore) InsertAudit(_ context.Context, id uuid.UUID, e auth.AuditEntry) error {
|
||||||
defer s.lock()()
|
defer s.lock()()
|
||||||
s.Audit = append(s.Audit, e)
|
s.Audit = append(s.Audit, e)
|
||||||
|
s.AuditIDs = append(s.AuditIDs, id)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListAudit реализует auth.Store.
|
||||||
|
func (s *MemStore) ListAudit(_ context.Context, before uuid.UUID, limit int) ([]auth.AuditRecord, error) {
|
||||||
|
defer s.lock()()
|
||||||
|
var out []auth.AuditRecord
|
||||||
|
for i := len(s.Audit) - 1; i >= 0 && len(out) < limit; i-- {
|
||||||
|
id, e := s.AuditIDs[i], s.Audit[i]
|
||||||
|
if before != uuid.Nil && id.String() >= before.String() {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
r := auth.AuditRecord{ID: id, ActorID: e.ActorID, Action: e.Action, TargetID: e.TargetID, Payload: e.Payload}
|
||||||
|
if e.TargetType != "" {
|
||||||
|
tt := e.TargetType
|
||||||
|
r.TargetType = &tt
|
||||||
|
}
|
||||||
|
if e.ActorID != nil {
|
||||||
|
if u, ok := s.Users[*e.ActorID]; ok {
|
||||||
|
name := u.Username
|
||||||
|
r.ActorUsername = &name
|
||||||
|
}
|
||||||
|
}
|
||||||
|
out = append(out, r)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// GetSettingBool реализует auth.Store.
|
// GetSettingBool реализует auth.Store.
|
||||||
func (s *MemStore) GetSettingBool(_ context.Context, key string) (bool, error) {
|
func (s *MemStore) GetSettingBool(_ context.Context, key string) (bool, error) {
|
||||||
defer s.lock()()
|
defer s.lock()()
|
||||||
|
|||||||
@@ -95,6 +95,18 @@ type Principal struct {
|
|||||||
MustChangeCredentials bool
|
MustChangeCredentials bool
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// AuditRecord — прочитанная запись audit_log с логином автора.
|
||||||
|
type AuditRecord struct {
|
||||||
|
ID uuid.UUID
|
||||||
|
ActorID *uuid.UUID
|
||||||
|
ActorUsername *string
|
||||||
|
Action string
|
||||||
|
TargetType *string
|
||||||
|
TargetID *uuid.UUID
|
||||||
|
Payload map[string]any
|
||||||
|
CreatedAt time.Time
|
||||||
|
}
|
||||||
|
|
||||||
// AuditEntry — запись audit_log.
|
// AuditEntry — запись audit_log.
|
||||||
type AuditEntry struct {
|
type AuditEntry struct {
|
||||||
ActorID *uuid.UUID
|
ActorID *uuid.UUID
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/google/uuid"
|
"github.com/google/uuid"
|
||||||
@@ -273,6 +274,135 @@ func (s *PgStore) ListSessions(ctx context.Context, userID uuid.UUID, now time.T
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListUsers реализует Store.
|
||||||
|
func (s *PgStore) ListUsers(ctx context.Context, f UserFilter) ([]User, error) {
|
||||||
|
var role *string
|
||||||
|
if f.Role != nil {
|
||||||
|
r := string(*f.Role)
|
||||||
|
role = &r
|
||||||
|
}
|
||||||
|
// Спецсимволы LIKE в запросе экранируются: ищем подстроку буквально.
|
||||||
|
pattern := "%" + strings.NewReplacer(`\`, `\\`, `%`, `\%`, `_`, `\_`).Replace(f.Query) + "%"
|
||||||
|
rows, err := s.q.Query(ctx, `
|
||||||
|
SELECT `+userColumns+` FROM users
|
||||||
|
WHERE ($1 = '%%' OR username ILIKE $1 OR display_name ILIKE $1)
|
||||||
|
AND ($2::user_role IS NULL OR role = $2::user_role)
|
||||||
|
AND ($3 = '' OR username > $3::citext)
|
||||||
|
ORDER BY username
|
||||||
|
LIMIT $4`, pattern, role, f.After, f.Limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("auth: list users: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
var out []User
|
||||||
|
for rows.Next() {
|
||||||
|
u, err := scanUser(rows)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
out = append(out, u)
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return nil, fmt.Errorf("auth: list users: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// LockActiveAdmins реализует Store.
|
||||||
|
func (s *PgStore) LockActiveAdmins(ctx context.Context) (int, error) {
|
||||||
|
rows, err := s.q.Query(ctx, `SELECT id FROM users WHERE role = 'admin' AND NOT is_blocked FOR UPDATE`)
|
||||||
|
if err != nil {
|
||||||
|
return 0, fmt.Errorf("auth: lock admins: %w", err)
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
n := 0
|
||||||
|
for rows.Next() {
|
||||||
|
n++
|
||||||
|
}
|
||||||
|
if err := rows.Err(); err != nil {
|
||||||
|
return 0, fmt.Errorf("auth: lock admins: %w", err)
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// UpdateUser реализует Store.
|
||||||
|
func (s *PgStore) UpdateUser(ctx context.Context, id uuid.UUID, upd UserUpdate) (User, error) {
|
||||||
|
var (
|
||||||
|
setName bool
|
||||||
|
name *string
|
||||||
|
role *string
|
||||||
|
)
|
||||||
|
if upd.DisplayName != nil {
|
||||||
|
setName, name = true, *upd.DisplayName
|
||||||
|
}
|
||||||
|
if upd.Role != nil {
|
||||||
|
r := string(*upd.Role)
|
||||||
|
role = &r
|
||||||
|
}
|
||||||
|
return scanUser(s.q.QueryRow(ctx, `
|
||||||
|
UPDATE users SET
|
||||||
|
display_name = CASE WHEN $2 THEN $3 ELSE display_name END,
|
||||||
|
role = COALESCE($4::user_role, role),
|
||||||
|
is_blocked = COALESCE($5, is_blocked),
|
||||||
|
token_version = token_version + CASE WHEN $6 THEN 1 ELSE 0 END,
|
||||||
|
updated_at = now()
|
||||||
|
WHERE id = $1
|
||||||
|
RETURNING `+userColumns, id, setName, name, role, upd.IsBlocked, upd.RevokeTokens))
|
||||||
|
}
|
||||||
|
|
||||||
|
// ResetPassword реализует Store.
|
||||||
|
func (s *PgStore) ResetPassword(ctx context.Context, id uuid.UUID, hash string) error {
|
||||||
|
_, err := s.q.Exec(ctx, `
|
||||||
|
UPDATE users SET password_hash = $2, must_change_password = true, failed_login_count = 0,
|
||||||
|
locked_until = NULL, token_version = token_version + 1, updated_at = now()
|
||||||
|
WHERE id = $1`, id, hash)
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("auth: reset password: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// DeleteUser реализует Store. Токены удаляются каскадно, ссылки в audit_log и created_by обнуляются.
|
||||||
|
func (s *PgStore) DeleteUser(ctx context.Context, id uuid.UUID) error {
|
||||||
|
if _, err := s.q.Exec(ctx, `DELETE FROM users WHERE id = $1`, id); err != nil {
|
||||||
|
return fmt.Errorf("auth: delete user: %w", err)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// ListAudit реализует Store.
|
||||||
|
func (s *PgStore) ListAudit(ctx context.Context, before uuid.UUID, limit int) ([]AuditRecord, error) {
|
||||||
|
rows, err := s.q.Query(ctx, `
|
||||||
|
SELECT a.id, a.actor_id, u.username::text, a.action, a.target_type, a.target_id, a.payload, a.created_at
|
||||||
|
FROM audit_log a LEFT JOIN users u ON u.id = a.actor_id
|
||||||
|
WHERE $1 = '00000000-0000-0000-0000-000000000000'::uuid OR a.id < $1
|
||||||
|
ORDER BY a.id DESC
|
||||||
|
LIMIT $2`, before, limit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("auth: list audit: %w", err)
|
||||||
|
}
|
||||||
|
out, err := pgx.CollectRows(rows, func(row pgx.CollectableRow) (AuditRecord, error) {
|
||||||
|
var (
|
||||||
|
r AuditRecord
|
||||||
|
payload []byte
|
||||||
|
)
|
||||||
|
if err := row.Scan(&r.ID, &r.ActorID, &r.ActorUsername, &r.Action, &r.TargetType, &r.TargetID, &payload,
|
||||||
|
&r.CreatedAt); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
if payload != nil {
|
||||||
|
if err := json.Unmarshal(payload, &r.Payload); err != nil {
|
||||||
|
return r, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return r, nil
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("auth: list audit: %w", err)
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
// InsertAudit реализует Store.
|
// InsertAudit реализует Store.
|
||||||
func (s *PgStore) InsertAudit(ctx context.Context, id uuid.UUID, e AuditEntry) error {
|
func (s *PgStore) InsertAudit(ctx context.Context, id uuid.UUID, e AuditEntry) error {
|
||||||
var payload []byte
|
var payload []byte
|
||||||
|
|||||||
@@ -21,6 +21,22 @@ type ProfileUpdate struct {
|
|||||||
UIPrefs *UIPrefs
|
UIPrefs *UIPrefs
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UserFilter — параметры списка пользователей.
|
||||||
|
type UserFilter struct {
|
||||||
|
Query string // подстрока логина или имени
|
||||||
|
Role *Role
|
||||||
|
After string // логин последнего элемента предыдущей страницы
|
||||||
|
Limit int
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserUpdate — изменения из админки. nil — не менять.
|
||||||
|
type UserUpdate struct {
|
||||||
|
DisplayName **string
|
||||||
|
Role *Role
|
||||||
|
IsBlocked *bool
|
||||||
|
RevokeTokens bool
|
||||||
|
}
|
||||||
|
|
||||||
// Store — доступ к users, refresh_tokens, audit_log и app_settings.
|
// Store — доступ к users, refresh_tokens, audit_log и app_settings.
|
||||||
type Store interface {
|
type Store interface {
|
||||||
// WithTx выполняет fn в транзакции; Store внутри fn работает в ней же.
|
// WithTx выполняет fn в транзакции; Store внутри fn работает в ней же.
|
||||||
@@ -49,7 +65,20 @@ type Store interface {
|
|||||||
// ListSessions — активные семьи пользователя: есть неиспользованный, неотозванный, не истёкший токен.
|
// ListSessions — активные семьи пользователя: есть неиспользованный, неотозванный, не истёкший токен.
|
||||||
ListSessions(ctx context.Context, userID uuid.UUID, now time.Time) ([]Session, error)
|
ListSessions(ctx context.Context, userID uuid.UUID, now time.Time) ([]Session, error)
|
||||||
|
|
||||||
|
// ListUsers — страница пользователей по возрастанию логина, после логина f.After.
|
||||||
|
ListUsers(ctx context.Context, f UserFilter) ([]User, error)
|
||||||
|
// LockActiveAdmins блокирует строки активных админов до конца транзакции и возвращает их число:
|
||||||
|
// два параллельных понижения не оставят систему без админа.
|
||||||
|
LockActiveAdmins(ctx context.Context) (int, error)
|
||||||
|
// UpdateUser применяет изменения админки; RevokeTokens увеличивает token_version.
|
||||||
|
UpdateUser(ctx context.Context, id uuid.UUID, upd UserUpdate) (User, error)
|
||||||
|
// ResetPassword ставит временный пароль: must_change_password, сброс блокировки входа, token_version + 1.
|
||||||
|
ResetPassword(ctx context.Context, id uuid.UUID, hash string) error
|
||||||
|
DeleteUser(ctx context.Context, id uuid.UUID) error
|
||||||
|
|
||||||
InsertAudit(ctx context.Context, id uuid.UUID, e AuditEntry) error
|
InsertAudit(ctx context.Context, id uuid.UUID, e AuditEntry) error
|
||||||
|
// ListAudit — записи журнала новее-первыми, с id меньше before (uuid.Nil — с начала).
|
||||||
|
ListAudit(ctx context.Context, before uuid.UUID, limit int) ([]AuditRecord, error)
|
||||||
GetSettingBool(ctx context.Context, key string) (bool, error)
|
GetSettingBool(ctx context.Context, key string) (bool, error)
|
||||||
SetSetting(ctx context.Context, key string, value any) error
|
SetSetting(ctx context.Context, key string, value any) error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,5 +57,9 @@ func NewRouter(d Deps) http.Handler {
|
|||||||
RequestErrorHandlerFunc: httpx.RequestError,
|
RequestErrorHandlerFunc: httpx.RequestError,
|
||||||
ResponseErrorHandlerFunc: httpx.ResponseError(log),
|
ResponseErrorHandlerFunc: httpx.ResponseError(log),
|
||||||
})
|
})
|
||||||
return gen.HandlerWithOptions(strict, gen.ChiServerOptions{BaseRouter: r})
|
return gen.HandlerWithOptions(strict, gen.ChiServerOptions{
|
||||||
|
BaseRouter: r,
|
||||||
|
// Ошибки разбора path- и query-параметров — тоже problem+json, а не текст по умолчанию.
|
||||||
|
ErrorHandlerFunc: httpx.RequestError,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user