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>
|
||
);
|
||
}
|