import type { AdminOperator, AdminOperatorCreateRequest, AdminOperatorProvisioning, AdminSecuritySummary, AdminLoginResponse, AuditLogEntry, CreditGrantRequest, CreditGrantResponse, LedgerEntry, Overview, PageResult, ReferralOverview, SessionResponse, UserDetail, UserSummary, } from "./types"; const API_BASE = "/v1/admin"; const REQUEST_TIMEOUT_MS = 12_000; interface ApiErrorBody { code?: string; error?: { code?: string; }; } export class ApiError extends Error { constructor( public readonly code: string, message: string, public readonly status: number, ) { super(message); this.name = "ApiError"; } } let csrfToken = ""; export function setCsrfToken(token?: string): void { csrfToken = token?.trim() ?? ""; } function csrfTokenFromCookie(): string { if (typeof document === "undefined") return ""; const prefix = "osg_admin_csrf="; return ( document.cookie .split(";") .map((part) => part.trim()) .find((part) => part.startsWith(prefix)) ?.slice(prefix.length) ?? "" ); } function safeMessage(status: number, code?: string): string { const messages: Record = { INVALID_CREDENTIALS: "用户名、密码或动态验证码错误", INVALID_TOTP: "动态验证码无效或已过期", VALIDATION_ERROR: "提交内容不符合要求", USER_NOT_FOUND: "未找到该用户", INSUFFICIENT_PERMISSION: "无权执行此操作", IDEMPOTENCY_CONFLICT: "该赠送请求与已有记录冲突,请核对审计日志", ADMIN_OPERATOR_NOT_FOUND: "未找到该管理员", ADMIN_USERNAME_CONFLICT: "该管理员用户名已存在", CANNOT_DISABLE_SELF: "不能停用当前登录的管理员", LAST_SUPER_ADMIN_REQUIRED: "必须至少保留一名启用的超级管理员", RATE_LIMITED: "操作过于频繁,请稍后再试", }; if (code && messages[code]) return messages[code]; if (status === 401) return "登录状态已失效,请重新登录"; if (status === 403) return "无权执行此操作"; if (status === 409) return "请求冲突,请刷新后重试"; if (status === 429) return "操作过于频繁,请稍后再试"; if (status >= 500) return "服务暂时不可用,请稍后再试"; return "请求失败,请检查后重试"; } async function request( path: string, options: RequestInit = {}, ): Promise { const method = options.method?.toUpperCase() ?? "GET"; const headers = new Headers(options.headers); headers.set("Accept", "application/json"); if (options.body) headers.set("Content-Type", "application/json"); const csrfRequired = !["GET", "HEAD", "OPTIONS"].includes(method) && path !== "/auth/login"; if (csrfRequired) { const requestToken = csrfToken || csrfTokenFromCookie() || headers.get("X-CSRF-Token") || ""; if (!requestToken) { throw new ApiError("CSRF_TOKEN_MISSING", "安全令牌缺失,请重新登录", 403); } headers.set("X-CSRF-Token", requestToken); } try { const response = await fetch(`${API_BASE}${path}`, { ...options, headers, credentials: "include", signal: options.signal ?? AbortSignal.timeout(REQUEST_TIMEOUT_MS), }); if (!response.ok) { let body: ApiErrorBody = {}; try { body = (await response.json()) as ApiErrorBody; } catch { // 非 JSON 错误响应不会暴露给界面。 } const apiError = new ApiError( body.code ?? body.error?.code ?? `HTTP_${response.status}`, safeMessage(response.status, body.code ?? body.error?.code), response.status, ); if ( response.status === 401 && !path.startsWith("/auth/") && typeof window !== "undefined" ) { window.dispatchEvent(new Event("admin:unauthorized")); } throw apiError; } if (response.status === 204) return undefined as T; return (await response.json()) as T; } catch (error) { if (error instanceof ApiError) throw error; if (error instanceof DOMException && error.name === "TimeoutError") { throw new ApiError("REQUEST_TIMEOUT", "请求超时,请稍后再试", 0); } throw new ApiError("NETWORK_ERROR", "网络连接失败,请检查网络", 0); } } function query(params: Record): string { const search = new URLSearchParams(); Object.entries(params).forEach(([key, value]) => { if (value) search.set(key, value); }); const result = search.toString(); return result ? `?${result}` : ""; } export const adminApi = { session: () => request("/auth/session"), login: (username: string, password: string, totpCode: string) => request("/auth/login", { method: "POST", body: JSON.stringify({ username, password, totpCode }), }), logout: () => request("/auth/logout", { method: "POST" }), overview: (range: string) => request(`/overview${query({ range })}`), referrals: (range: string) => request(`/referrals${query({ range })}`), users: (search = "", cursor?: string) => request>( `/users${query({ q: search.trim(), cursor })}`, ), user: (userId: string) => request(`/users/${encodeURIComponent(userId)}`), latestLedger: (cursor?: string) => request>(`/credits/ledger${query({ cursor })}`), ledger: (userId: string, cursor?: string) => request>( `/users/${encodeURIComponent(userId)}/ledger${query({ cursor })}`, ), grantCredits: (payload: CreditGrantRequest) => request("/credits/grants", { method: "POST", body: JSON.stringify({ userId: payload.userId, amount: payload.amount, reason: payload.reason, }), headers: { "Idempotency-Key": payload.idempotencyKey }, }), auditLogs: (cursor?: string) => request>(`/audit${query({ cursor })}`), operators: (cursor?: string) => request>(`/operators${query({ cursor })}`), operatorSummary: () => request("/operators/summary"), createOperator: (payload: AdminOperatorCreateRequest) => request("/operators", { method: "POST", body: JSON.stringify(payload), }), setOperatorEnabled: (operatorId: string, enabled: boolean) => request( `/operators/${encodeURIComponent(operatorId)}/${enabled ? "enable" : "disable"}`, { method: "POST" }, ), unlockOperator: (operatorId: string) => request(`/operators/${encodeURIComponent(operatorId)}/unlock`, { method: "POST", }), resetOperatorCredentials: (operatorId: string, password: string) => request( `/operators/${encodeURIComponent(operatorId)}/credentials/reset`, { method: "POST", body: JSON.stringify({ password }), }, ), revokeOperatorSessions: (operatorId: string) => request( `/operators/${encodeURIComponent(operatorId)}/sessions/revoke`, { method: "POST" }, ), };