Files
ForbiddenStarsApp/frontend/src/hooks/admin.ts
T

252 lines
7.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useMutation, useQuery, useQueryClient } from "@tanstack/react-query";
import { ApiError, api, unwrap } from "../api/client";
import { qk } from "../api/queryKeys";
import type { AdminMe, MatchUpdate } from "../domain/types";
function readCsrfToken(): string | null {
const m = document.cookie.match(/(?:^|; )csrf_token=([^;]*)/);
return m ? decodeURIComponent(m[1]) : null;
}
export function useAdminMe() {
return useQuery({
queryKey: qk.adminMe,
queryFn: async (): Promise<AdminMe | null> => {
const r = await api.GET("/api/admin/me");
if (r.response.status === 401 || r.response.status === 403) return null;
return unwrap(r);
},
});
}
export function useAdminLogin() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: { username: string; password: string }) =>
unwrap(await api.POST("/api/admin/auth/login", { body })),
onSuccess: (me) => qc.setQueryData(qk.adminMe, me),
});
}
export function useAdminLogout() {
const qc = useQueryClient();
return useMutation({
mutationFn: async () => unwrap(await api.POST("/api/admin/auth/logout")),
onSuccess: () => qc.setQueryData(qk.adminMe, null),
});
}
export function useAdminUsers(query?: string) {
return useQuery({
queryKey: [...qk.adminUsers, query ?? ""],
queryFn: async () =>
unwrap(
await api.GET("/api/admin/users", {
params: { query: query ? { query } : {} },
}),
),
});
}
export function useAdminGroups() {
return useQuery({
queryKey: qk.adminGroups,
queryFn: async () => unwrap(await api.GET("/api/admin/groups")),
});
}
export interface AdminMatchFilters {
group_id?: number | null;
user_id?: number | null;
faction_id?: number | null;
}
export function useAdminMatches(filters: AdminMatchFilters = {}) {
const query: Record<string, number> = {};
if (filters.group_id != null) query.group_id = filters.group_id;
if (filters.user_id != null) query.user_id = filters.user_id;
if (filters.faction_id != null) query.faction_id = filters.faction_id;
return useQuery({
queryKey: [...qk.adminMatches, query],
queryFn: async () =>
unwrap(await api.GET("/api/admin/matches", { params: { query } })),
});
}
export function useAdminMatch(matchId: number | null) {
return useQuery({
queryKey: ["adminMatch", matchId],
enabled: matchId != null,
queryFn: async () =>
unwrap(
await api.GET("/api/admin/matches/{match_id}", {
params: { path: { match_id: matchId as number } },
}),
),
});
}
export function useAdminUpdateMatch() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (args: { matchId: number; body: MatchUpdate }) =>
unwrap(
await api.PATCH("/api/admin/matches/{match_id}", {
params: { path: { match_id: args.matchId } },
body: args.body,
}),
),
onSuccess: (_d, args) => {
qc.invalidateQueries({ queryKey: qk.adminMatches });
qc.invalidateQueries({ queryKey: ["adminMatch", args.matchId] });
},
});
}
export function useAdminFactions() {
return useQuery({
queryKey: ["adminFactions"],
queryFn: async () => unwrap(await api.GET("/api/admin/factions")),
});
}
export function useAdminRenameFaction() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (args: { factionId: number; name_ru: string }) =>
unwrap(
await api.PATCH("/api/admin/factions/{faction_id}", {
params: { path: { faction_id: args.factionId } },
body: { name_ru: args.name_ru },
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: ["adminFactions"] }),
});
}
export function useAdminLogs(filters?: { action?: string; entity_type?: string }) {
return useQuery({
queryKey: [...qk.adminLogs, filters?.action ?? "", filters?.entity_type ?? ""],
queryFn: async () =>
unwrap(
await api.GET("/api/admin/audit-logs", {
params: {
query: {
...(filters?.action ? { action: filters.action } : {}),
...(filters?.entity_type ? { entity_type: filters.entity_type } : {}),
},
},
}),
),
});
}
export function useAdminDeleteMatch() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (matchId: number) =>
unwrap(
await api.DELETE("/api/admin/matches/{match_id}", {
params: { path: { match_id: matchId } },
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminMatches }),
});
}
export function useAdminUpdateUser() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (args: { userId: number; nickname?: string; is_active?: boolean }) =>
unwrap(
await api.PATCH("/api/admin/users/{user_id}", {
params: { path: { user_id: args.userId } },
body: { nickname: args.nickname, is_active: args.is_active },
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminUsers }),
});
}
// ─── Ачивки (редактор; выдача игрокам — на будущее) ──────────────────────────
interface AchievementInput {
name?: string;
description?: string;
condition?: string;
}
export function useAdminAchievements() {
return useQuery({
queryKey: qk.adminAchievements,
queryFn: async () => unwrap(await api.GET("/api/admin/achievements")),
});
}
export function useCreateAchievement() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (body: AchievementInput & { name: string }) =>
unwrap(await api.POST("/api/admin/achievements", { body })),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
});
}
export function useUpdateAchievement() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (args: { slug: string; body: AchievementInput }) =>
unwrap(
await api.PATCH("/api/admin/achievements/{slug}", {
params: { path: { slug: args.slug } },
body: args.body,
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
});
}
export function useDeleteAchievement() {
const qc = useQueryClient();
return useMutation({
mutationFn: async (slug: string) =>
unwrap(
await api.DELETE("/api/admin/achievements/{slug}", {
params: { path: { slug } },
}),
),
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
});
}
// Иконка ачивки — multipart отдельным fetch (как аватар), с CSRF из cookie.
export function useUploadAchievementIcon() {
const qc = useQueryClient();
return useMutation({
mutationFn: async ({ slug, file }: { slug: string; file: File }) => {
const form = new FormData();
form.append("file", file, file.name);
const csrf = readCsrfToken();
const base = import.meta.env.VITE_API_BASE_URL || "";
const r = await fetch(`${base}/api/admin/achievements/${slug}/icon`, {
method: "PUT",
body: form,
credentials: "include",
headers: csrf ? { "X-CSRF-Token": csrf } : {},
});
if (!r.ok) {
let env: { code?: string; message?: string } | undefined;
try {
env = ((await r.json()) as { error?: typeof env }).error;
} catch {
/* тело без JSON */
}
throw new ApiError(env?.message || "Не удалось загрузить", env?.code || "ERROR", r.status);
}
return await r.json();
},
onSuccess: () => qc.invalidateQueries({ queryKey: qk.adminAchievements }),
});
}