"use client"; import type { AccessControlProvider, AuthProvider, BaseRecord, CrudFilter, DataProvider, HttpError, CreateParams, DeleteOneParams, GetListParams, GetOneParams, UpdateParams, } from "@refinedev/core"; export type AdminIdentity = { id: string; email: string; name: string; roles: string[]; permissions: string[]; requiresMfa: boolean; }; const apiBase = "/api/admin"; let identityCache: AdminIdentity | null = null; function logicalFilters(filters: CrudFilter[] | undefined) { return (filters ?? []).filter( (filter): filter is Extract => "field" in filter, ); } export async function adminRequestJson( url: string, init?: RequestInit, ): Promise { const response = await fetch(url, { cache: "no-store", credentials: "same-origin", ...init, headers: { ...(init?.body ? { "content-type": "application/json" } : {}), ...init?.headers, }, }); const payload = await response.json().catch(() => null); if (!response.ok) { const message = payload && typeof payload.error === "string" ? payload.error : "后台请求失败"; throw Object.assign(new Error(message), { statusCode: response.status, } satisfies Partial); } return payload as T; } function listParams( pagination: { currentPage?: number; pageSize?: number } | undefined, sorters: { field: string; order: "asc" | "desc" }[] | undefined, filters: CrudFilter[] | undefined, ) { const search = new URLSearchParams({ page: String(pagination?.currentPage ?? 1), pageSize: String(pagination?.pageSize ?? 20), }); const sorter = sorters?.[0]; if (sorter) { search.set("sort", sorter.field); search.set("order", sorter.order); } for (const filter of logicalFilters(filters)) { if ( filter.value === undefined || filter.value === null || filter.value === "" ) continue; if (filter.field === "q" || filter.field === "status") { search.set(filter.field, String(filter.value)); } } return search; } export const adminDataProvider: DataProvider = { async getList({ resource, pagination, sorters, filters, }: GetListParams) { const search = listParams(pagination, sorters, filters); return adminRequestJson<{ data: TData[]; total: number }>( `${apiBase}/${resource}?${search}`, ); }, async getOne({ resource, id }: GetOneParams) { return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}/${id}`); }, async create({ resource, variables, }: CreateParams) { return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}`, { method: "POST", body: JSON.stringify(variables), }); }, async update({ resource, id, variables, }: UpdateParams) { return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}/${id}`, { method: "PATCH", body: JSON.stringify(variables), }); }, async deleteOne({ resource, id, }: DeleteOneParams) { return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}/${id}`, { method: "DELETE", }); }, getApiUrl: () => apiBase, }; async function loadIdentity(): Promise { if (identityCache) return identityCache; const payload = await adminRequestJson<{ user: AdminIdentity }>( `${apiBase}/session`, ); identityCache = payload.user; return identityCache; } export const adminAuthProvider: AuthProvider = { async login() { return { success: false, redirectTo: "/login" }; }, async logout() { identityCache = null; await fetch("/api/auth/sign-out", { method: "POST", credentials: "same-origin", }); return { success: true, redirectTo: "/login" }; }, async check() { try { await loadIdentity(); return { authenticated: true }; } catch (error) { const status = (error as HttpError).statusCode; return { authenticated: false, redirectTo: status === 401 ? "/login" : "/", error: error as HttpError, }; } }, async onError(error) { const status = (error as HttpError)?.statusCode; if (status === 401) return { redirectTo: "/login", logout: true }; if (status === 403) return { error: error as HttpError }; return { error: error as HttpError }; }, async getPermissions() { return (await loadIdentity()).permissions; }, async getIdentity() { return loadIdentity(); }, }; const resourcePermissions: Record = { administrators: { read: "admin.users.read", write: "admin.users.manage_roles", }, roles: { read: "admin.users.read" }, customers: { read: "admin.customers.read" }, "account-deletions": { read: "admin.customers.read" }, codes: { read: "admin.access", write: "admin.access" }, "credit-transactions": { read: "billing.orders.read" }, consultations: { read: "billing.orders.read" }, "audit-logs": { read: "audit.read" }, "moderation-events": { read: "audit.read" }, payments: { read: "billing.orders.read" }, packages: { read: "billing.products.read", write: "billing.products.write" }, products: { read: "billing.products.read", write: "billing.products.write" }, "feature-pricing": { read: "billing.products.read", write: "billing.products.write" }, "pricing-simulator": { read: "billing.products.read" }, subscriptions: { read: "billing.orders.read", write: "billing.adjustments.write", }, orders: { read: "billing.orders.read", write: "billing.adjustments.write" }, usage: { read: "billing.orders.read" }, "rectification-telemetry": { read: "admin.customers.read" }, feedback: { read: "support.feedback.read", write: "support.feedback.write" }, models: { read: "models.read", write: "models.write" }, "model-releases": { read: "models.read", write: "models.publish" }, "feature-flags": { read: "admin.access", write: "ops.flags.write" }, security: { read: "admin.access", write: "admin.access" }, }; export const adminAccessControlProvider: AccessControlProvider = { async can({ resource, action }) { const identity = await loadIdentity(); const required = resourcePermissions[resource ?? ""]; if (!required) return { can: false, reason: "未知管理资源" }; const permission = action === "list" || action === "show" ? required.read : required.write; return permission && identity.permissions.includes(permission) ? { can: true } : { can: false, reason: "无此操作权限" }; }, options: { buttons: { enableAccessControl: true, hideIfUnauthorized: true }, }, };