Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01N4f2nya58RoRu4yEmJgRGE
230 lines
6.8 KiB
TypeScript
230 lines
6.8 KiB
TypeScript
"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<CrudFilter, { field: string }> =>
|
|
"field" in filter,
|
|
);
|
|
}
|
|
|
|
export async function adminRequestJson<T>(
|
|
url: string,
|
|
init?: RequestInit,
|
|
): Promise<T> {
|
|
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<HttpError>);
|
|
}
|
|
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<TData extends BaseRecord>({
|
|
resource,
|
|
pagination,
|
|
sorters,
|
|
filters,
|
|
}: GetListParams) {
|
|
const search = listParams(pagination, sorters, filters);
|
|
return adminRequestJson<{ data: TData[]; total: number }>(
|
|
`${apiBase}/${resource}?${search}`,
|
|
);
|
|
},
|
|
async getOne<TData extends BaseRecord>({ resource, id }: GetOneParams) {
|
|
return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}/${id}`);
|
|
},
|
|
async create<TData extends BaseRecord, TVariables>({
|
|
resource,
|
|
variables,
|
|
}: CreateParams<TVariables>) {
|
|
return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}`, {
|
|
method: "POST",
|
|
body: JSON.stringify(variables),
|
|
});
|
|
},
|
|
async update<TData extends BaseRecord, TVariables>({
|
|
resource,
|
|
id,
|
|
variables,
|
|
}: UpdateParams<TVariables>) {
|
|
return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}/${id}`, {
|
|
method: "PATCH",
|
|
body: JSON.stringify(variables),
|
|
});
|
|
},
|
|
async deleteOne<TData extends BaseRecord, TVariables>({
|
|
resource,
|
|
id,
|
|
}: DeleteOneParams<TVariables>) {
|
|
return adminRequestJson<{ data: TData }>(`${apiBase}/${resource}/${id}`, {
|
|
method: "DELETE",
|
|
});
|
|
},
|
|
getApiUrl: () => apiBase,
|
|
};
|
|
|
|
async function loadIdentity(): Promise<AdminIdentity> {
|
|
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<string, { read: string; write?: string }> = {
|
|
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 },
|
|
},
|
|
};
|