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

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