Добавлена функция удаления аккаунтов с каскадным удалением из всех записей для дева

This commit is contained in:
2026-06-17 16:34:29 +03:00
parent 865c967d5e
commit 3291c67b2c
9 changed files with 271 additions and 73 deletions
+49 -33
View File
@@ -518,8 +518,7 @@ export interface paths {
get?: never;
put?: never;
post?: never;
/** Delete User */
delete: operations["delete_user_api_admin_users__user_id__delete"];
delete?: never;
options?: never;
head?: never;
/** Update User */
@@ -682,6 +681,23 @@ export interface paths {
patch?: never;
trace?: never;
};
"/api/admin/dev/users/{user_id}": {
parameters: {
query?: never;
header?: never;
path?: never;
cookie?: never;
};
get?: never;
put?: never;
post?: never;
/** Delete User Hard */
delete: operations["delete_user_hard_api_admin_dev_users__user_id__delete"];
options?: never;
head?: never;
patch?: never;
trace?: never;
};
"/api/health": {
parameters: {
query?: never;
@@ -2479,37 +2495,6 @@ export interface operations {
};
};
};
delete_user_api_admin_users__user_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["OkResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
update_user_api_admin_users__user_id__patch: {
parameters: {
query?: never;
@@ -2903,6 +2888,37 @@ export interface operations {
};
};
};
delete_user_hard_api_admin_dev_users__user_id__delete: {
parameters: {
query?: never;
header?: never;
path: {
user_id: number;
};
cookie?: never;
};
requestBody?: never;
responses: {
/** @description Successful Response */
200: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["OkResponse"];
};
};
/** @description Validation Error */
422: {
headers: {
[name: string]: unknown;
};
content: {
"application/json": components["schemas"]["HTTPValidationError"];
};
};
};
};
health_api_health_get: {
parameters: {
query?: never;
+19 -11
View File
@@ -1,6 +1,9 @@
import { Spinner } from "../../components/Spinner";
import { useToast } from "../../context/ToastContext";
import { useAdminUpdateUser, useAdminUsers } from "../../hooks/admin";
// DEV-ТОЛЬКО: удаление аккаунтов. Импорт используется лишь под import.meta.env.DEV,
// поэтому в прод/тест-сборке вырезается тришейкингом (как и dev-вход).
import { DevDeleteAccountButton } from "./DevDeleteAccountButton";
export function AdminAccountsPage() {
const { data: users, isLoading } = useAdminUsers();
@@ -23,17 +26,22 @@ export function AdminAccountsPage() {
</div>
</div>
{u.role !== "admin" && (
<button
className="btn small"
onClick={async () => {
await update
.mutateAsync({ userId: u.id, is_active: !u.is_active })
.catch(() => {});
toast.show("Сохранено");
}}
>
{u.is_active ? "Отключить" : "Включить"}
</button>
<div className="row">
{import.meta.env.DEV && (
<DevDeleteAccountButton userId={u.id} nickname={u.nickname} />
)}
<button
className="btn small"
onClick={async () => {
await update
.mutateAsync({ userId: u.id, is_active: !u.is_active })
.catch(() => {});
toast.show("Сохранено");
}}
>
{u.is_active ? "Отключить" : "Включить"}
</button>
</div>
)}
</div>
</div>
@@ -0,0 +1,60 @@
import { useMutation, useQueryClient } from "@tanstack/react-query";
import { ApiError, api, unwrap } from "../../api/client";
import { qk } from "../../api/queryKeys";
import { useToast } from "../../context/ToastContext";
/**
* DEV-ТОЛЬКО кнопка жёсткого удаления аккаунта.
*
* Эндпоинт `DELETE /api/admin/dev/users/{id}` существует только в dev-сборке бэкенда
* (backend/app/routers/dev_admin.py, исключён из прод/тест-образа). Этот модуль
* рендерится лишь под `import.meta.env.DEV` в AdminAccountsPage, поэтому в прод-сборке
* он не используется и вырезается тришейкингом — в прод/тест удаление недоступно.
*/
export function DevDeleteAccountButton({
userId,
nickname,
}: {
userId: number;
nickname: string;
}) {
const qc = useQueryClient();
const toast = useToast();
const del = useMutation({
mutationFn: async () =>
unwrap(
await api.DELETE("/api/admin/dev/users/{user_id}", {
params: { path: { user_id: userId } },
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminUsers }),
});
return (
<button
className="btn small"
// Яркая (сплошная) красная кнопка. Стиль инлайном, а не классом в общем CSS,
// чтобы в прод/тест-стили не попало ничего, связанного с удалением.
style={{ background: "var(--danger)", borderColor: "var(--danger)", color: "#fff" }}
disabled={del.isPending}
onClick={async () => {
if (
!window.confirm(
`Удалить аккаунт «${nickname}»? Партии сохранятся, но игрок будет ` +
`вычеркнут из них. Действие необратимо.`,
)
)
return;
try {
await del.mutateAsync();
toast.show("Аккаунт удалён");
} catch (e) {
toast.show(e instanceof ApiError ? e.message : "Не удалось удалить");
}
}}
>
Удалить
</button>
);
}