Files
Jyotisha/frontend/src/components/admin/administrators-resource.tsx
T
2026-08-06 20:15:08 +08:00

167 lines
6.6 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 { ReasonActionModal } from "./reason-action-modal";
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 PendingRoleAction = {
action: "assign" | "revoke";
roleCode: RoleCode;
userId?: string;
email?: string;
label: 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 [pendingAction, setPendingAction] = useState<PendingRoleAction | 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);
}
function prepareAssignment(values: AssignmentForm) {
const role = roleOptions.find((item) => item.value === values.roleCode)!;
setPendingAction({
action: "assign",
roleCode: values.roleCode,
...(assignmentUser ? { userId: assignmentUser.id } : { email: values.email.trim() }),
label: `${assignmentUser?.email ?? values.email.trim()} · ${role.label}`,
});
}
async function submitRoleAction(reason: string) {
if (!pendingAction) return;
setSaving(true);
try {
await adminRequestJson("/api/admin/administrators", {
method: pendingAction.action === "assign" ? "POST" : "DELETE",
body: JSON.stringify({
userId: pendingAction.userId,
email: pendingAction.email,
roleCode: pendingAction.roleCode,
reason,
}),
});
message.success(pendingAction.action === "assign" ? "管理员角色已分配" : "管理员角色已撤销");
setPendingAction(null);
setAssignmentOpen(false);
setAssignmentUser(null);
assignmentForm.resetFields();
await tableQuery.refetch();
} finally {
setSaving(false);
}
}
return (
<Card
title="管理员"
extra={canManage ? <Button type="primary" icon={<PlusOutlined />} onClick={() => openAssignment()}></Button> : <Tag color="gold">RBAC</Tag>}
>
<Typography.Paragraph type="secondary">
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();
setPendingAction({ action: "revoke", roleCode: role, userId: item.id, label: `${item.email} · ${role}` });
}}
>{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="取消"
onOk={() => assignmentForm.submit()}
onCancel={() => setAssignmentOpen(false)}
destroyOnHidden
>
<Form<AssignmentForm> form={assignmentForm} layout="vertical" onFinish={prepareAssignment}>
<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>
<ReasonActionModal
open={Boolean(pendingAction)}
title={pendingAction?.action === "revoke" ? `撤销角色:${pendingAction.label}` : `分配角色:${pendingAction?.label ?? ""}`}
okText={pendingAction?.action === "revoke" ? "验证并撤销" : "验证并分配"}
danger={pendingAction?.action === "revoke"}
confirmLoading={saving}
reauthPermission="admin.users.manage_roles"
onCancel={() => setPendingAction(null)}
onSubmit={submitRoleAction}
/>
</Card>
);
}