152 lines
5.8 KiB
TypeScript
152 lines
5.8 KiB
TypeScript
"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>
|
|
);
|
|
}
|