Files
Jyotisha/frontend/src/components/admin/administrators-resource.tsx
T
Jesse_Chen 945d61fa1c
Independent Staging Quality Gate / validate (push) Failing after 12m38s
Independent Staging Quality Gate / publish (push) Has been skipped
fix(admin): remove redundant confirmations and repair code access
2026-08-16 17:55:15 +08:00

152 lines
5.8 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.
"use client";
import { PlusOutlined } from "@ant-design/icons";
import { useGetIdentity } from "@refinedev/core";
import { useTable } from "@refinedev/antd";
import { App, Button, Card, Form, Input, Modal, Select, Space, Table, Tag, Typography } from "antd";
import { useState } from "react";
import { adminRequestJson, type AdminIdentity } from "@/lib/admin/providers";
import { formatAdminDate } from "./resource-table";
const roleOptions = [
{ value: "owner", label: "Owner" },
{ value: "model_admin", label: "Model Admin" },
{ value: "billing_admin", label: "Billing Admin" },
{ value: "operations", label: "Operations" },
{ value: "support", label: "Support" },
{ value: "auditor", label: "Auditor" },
] as const;
type RoleCode = (typeof roleOptions)[number]["value"];
interface Administrator {
id: string;
email: string;
name: string;
roles: RoleCode[];
createdAt: string;
}
type AssignmentForm = { email: string; roleCode: RoleCode };
type RoleAction = {
action: "assign" | "revoke";
roleCode: RoleCode;
userId?: string;
email?: string;
};
export default function AdministratorsResource() {
const { message } = App.useApp();
const { data: identity } = useGetIdentity<AdminIdentity>();
const { tableProps, searchFormProps, tableQuery } = useTable<Administrator, { message: string; statusCode: number }, { q?: string }>({
resource: "administrators",
syncWithLocation: true,
pagination: { pageSize: 20 },
onSearch: ({ q }) => [{ field: "q", operator: "contains", value: q }],
});
const [assignmentForm] = Form.useForm<AssignmentForm>();
const [assignmentOpen, setAssignmentOpen] = useState(false);
const [assignmentUser, setAssignmentUser] = useState<Administrator | null>(null);
const [saving, setSaving] = useState(false);
const canManage = Boolean(identity?.permissions.includes("admin.users.manage_roles"));
function openAssignment(item?: Administrator) {
setAssignmentUser(item ?? null);
assignmentForm.setFieldsValue({ email: item?.email ?? "", roleCode: undefined });
setAssignmentOpen(true);
}
async function assignRole(values: AssignmentForm) {
await submitRoleAction({
action: "assign",
roleCode: values.roleCode,
...(assignmentUser ? { userId: assignmentUser.id } : { email: values.email.trim() }),
});
}
async function submitRoleAction(action: RoleAction) {
setSaving(true);
try {
await adminRequestJson("/api/admin/administrators", {
method: action.action === "assign" ? "POST" : "DELETE",
body: JSON.stringify({
userId: action.userId,
email: action.email,
roleCode: action.roleCode,
}),
});
message.success(action.action === "assign" ? "管理员角色已分配" : "管理员角色已撤销");
setAssignmentOpen(false);
setAssignmentUser(null);
assignmentForm.resetFields();
await tableQuery.refetch();
} catch (error) {
message.error(error instanceof Error ? error.message : "管理员角色操作失败");
} finally {
setSaving(false);
}
}
return (
<Card
title="管理员"
extra={canManage ? <Button type="primary" icon={<PlusOutlined />} onClick={() => openAssignment()}></Button> : <Typography.Text type="secondary"></Typography.Text>}
>
<Typography.Paragraph type="secondary">
ID Owner
</Typography.Paragraph>
<Form {...searchFormProps} layout="inline" style={{ marginBottom: 16 }}>
<Form.Item name="q" label="搜索">
<Input.Search allowClear placeholder="管理员邮箱或姓名" />
</Form.Item>
</Form>
<Table
{...tableProps}
rowKey="id"
scroll={{ x: "max-content" }}
columns={[
{ title: "邮箱", dataIndex: "email" },
{ title: "姓名", dataIndex: "name", render: (value: string) => value || "—" },
{
title: "角色",
dataIndex: "roles",
render: (roles: RoleCode[], item: Administrator) => <Space wrap>{roles.map((role) => <Tag
key={role}
closable={canManage}
onClose={(event) => {
event.preventDefault();
void submitRoleAction({ action: "revoke", roleCode: role, userId: item.id });
}}
>{role}</Tag>)}</Space>,
},
{ title: "加入时间", dataIndex: "createdAt", render: formatAdminDate },
...(canManage ? [{ title: "操作", fixed: "right" as const, render: (_: unknown, item: Administrator) => <Button type="link" onClick={() => openAssignment(item)}></Button> }] : []),
]}
/>
<Modal
title={assignmentUser ? `${assignmentUser.email} 分配角色` : "按邮箱分配管理员角色"}
open={assignmentOpen}
okText="分配"
cancelText="取消"
confirmLoading={saving}
onOk={() => assignmentForm.submit()}
onCancel={() => setAssignmentOpen(false)}
destroyOnHidden
>
<Form<AssignmentForm> form={assignmentForm} layout="vertical" onFinish={(values) => void assignRole(values)}>
<Form.Item name="email" label="用户邮箱" rules={[{ required: true }, { type: "email" }]}>
<Input disabled={Boolean(assignmentUser)} autoComplete="email" />
</Form.Item>
<Form.Item name="roleCode" label="角色" rules={[{ required: true }]}>
<Select
placeholder="选择要分配的角色"
options={roleOptions.filter((role) => !assignmentUser?.roles.includes(role.value))}
/>
</Form.Item>
</Form>
</Modal>
</Card>
);
}