feat(auth): add role/permission management (backend + frontend)

- RoleService: CRUD, assign_permissions, get_role_permissions
- PermissionService: list all tenant permissions
- Role handlers: 8 endpoints with RBAC permission checks
- Frontend Roles page: table, create/edit modal, permission assignment
- Frontend Roles API: full CRUD + permission operations
- Routes registered in AuthModule protected_routes
This commit is contained in:
iven
2026-04-11 03:46:54 +08:00
parent 4a03a639a6
commit 6fd0288e7c
9 changed files with 946 additions and 2 deletions

View File

@@ -0,0 +1,272 @@
import { useState, useEffect, useCallback } from 'react';
import {
Table,
Button,
Space,
Modal,
Form,
Input,
Tag,
Popconfirm,
Checkbox,
message,
Typography,
} from 'antd';
import { PlusOutlined } from '@ant-design/icons';
import {
listRoles,
createRole,
updateRole,
deleteRole,
assignPermissions,
getRolePermissions,
listPermissions,
type RoleInfo,
type PermissionInfo,
} from '../api/roles';
export default function Roles() {
const [roles, setRoles] = useState<RoleInfo[]>([]);
const [permissions, setPermissions] = useState<PermissionInfo[]>([]);
const [loading, setLoading] = useState(false);
const [createModalOpen, setCreateModalOpen] = useState(false);
const [editRole, setEditRole] = useState<RoleInfo | null>(null);
const [permModalOpen, setPermModalOpen] = useState(false);
const [selectedRole, setSelectedRole] = useState<RoleInfo | null>(null);
const [selectedPermIds, setSelectedPermIds] = useState<string[]>([]);
const [form] = Form.useForm();
const fetchRoles = useCallback(async () => {
setLoading(true);
try {
const result = await listRoles();
setRoles(result.data);
} catch {
message.error('加载角色失败');
}
setLoading(false);
}, []);
const fetchPermissions = useCallback(async () => {
try {
setPermissions(await listPermissions());
} catch {
// Permissions may not be seeded yet; silently ignore
}
}, []);
useEffect(() => {
fetchRoles();
fetchPermissions();
}, [fetchRoles, fetchPermissions]);
const handleCreate = async (values: {
name: string;
code: string;
description?: string;
}) => {
try {
if (editRole) {
await updateRole(editRole.id, values);
message.success('角色更新成功');
} else {
await createRole(values);
message.success('角色创建成功');
}
setCreateModalOpen(false);
setEditRole(null);
form.resetFields();
fetchRoles();
} catch (err: unknown) {
const errorMsg =
(err as { response?: { data?: { message?: string } } })?.response?.data
?.message || '操作失败';
message.error(errorMsg);
}
};
const handleDelete = async (id: string) => {
try {
await deleteRole(id);
message.success('角色已删除');
fetchRoles();
} catch {
message.error('删除失败');
}
};
const openPermModal = async (role: RoleInfo) => {
setSelectedRole(role);
try {
const rolePerms = await getRolePermissions(role.id);
setSelectedPermIds(rolePerms.map((p) => p.id));
} catch {
setSelectedPermIds([]);
}
setPermModalOpen(true);
};
const savePermissions = async () => {
if (!selectedRole) return;
try {
await assignPermissions(selectedRole.id, selectedPermIds);
message.success('权限分配成功');
setPermModalOpen(false);
} catch {
message.error('权限分配失败');
}
};
const openEditModal = (role: RoleInfo) => {
setEditRole(role);
form.setFieldsValue({
name: role.name,
code: role.code,
description: role.description,
});
setCreateModalOpen(true);
};
const openCreateModal = () => {
setEditRole(null);
form.resetFields();
setCreateModalOpen(true);
};
const closeCreateModal = () => {
setCreateModalOpen(false);
setEditRole(null);
form.resetFields();
};
const columns = [
{ title: '名称', dataIndex: 'name', key: 'name' },
{ title: '编码', dataIndex: 'code', key: 'code' },
{
title: '描述',
dataIndex: 'description',
key: 'description',
ellipsis: true,
},
{
title: '类型',
dataIndex: 'is_system',
key: 'is_system',
render: (v: boolean) =>
v ? <Tag color="blue"></Tag> : <Tag></Tag>,
},
{
title: '操作',
key: 'actions',
render: (_: unknown, record: RoleInfo) => (
<Space>
<Button size="small" onClick={() => openPermModal(record)}>
</Button>
{!record.is_system && (
<>
<Button size="small" onClick={() => openEditModal(record)}>
</Button>
<Popconfirm
title="确定删除此角色?"
onConfirm={() => handleDelete(record.id)}
>
<Button size="small" danger>
</Button>
</Popconfirm>
</>
)}
</Space>
),
},
];
// Group permissions by resource for better UX
const groupedPermissions = permissions.reduce<Record<string, PermissionInfo[]>>(
(acc, p) => {
if (!acc[p.resource]) acc[p.resource] = [];
acc[p.resource].push(p);
return acc;
},
{},
);
return (
<div>
<div
style={{
display: 'flex',
justifyContent: 'space-between',
marginBottom: 16,
}}
>
<Typography.Title level={4} style={{ margin: 0 }}>
</Typography.Title>
<Button type="primary" icon={<PlusOutlined />} onClick={openCreateModal}>
</Button>
</div>
<Table
columns={columns}
dataSource={roles}
rowKey="id"
loading={loading}
pagination={{ pageSize: 20 }}
/>
<Modal
title={editRole ? '编辑角色' : '新建角色'}
open={createModalOpen}
onCancel={closeCreateModal}
onOk={() => form.submit()}
>
<Form form={form} onFinish={handleCreate} layout="vertical">
<Form.Item
name="name"
label="名称"
rules={[{ required: true, message: '请输入角色名称' }]}
>
<Input />
</Form.Item>
<Form.Item
name="code"
label="编码"
rules={[{ required: true, message: '请输入角色编码' }]}
>
<Input disabled={!!editRole} />
</Form.Item>
<Form.Item name="description" label="描述">
<Input.TextArea rows={3} />
</Form.Item>
</Form>
</Modal>
<Modal
title={`权限分配 - ${selectedRole?.name || ''}`}
open={permModalOpen}
onCancel={() => setPermModalOpen(false)}
onOk={savePermissions}
width={600}
>
{Object.entries(groupedPermissions).map(([resource, perms]) => (
<div key={resource} style={{ marginBottom: 16 }}>
<Typography.Text strong style={{ textTransform: 'capitalize' }}>
{resource}
</Typography.Text>
<div style={{ marginTop: 8 }}>
<Checkbox.Group
value={selectedPermIds}
onChange={(values) => setSelectedPermIds(values as string[])}
options={perms.map((p) => ({ label: p.name, value: p.id }))}
/>
</div>
</div>
))}
</Modal>
</div>
);
}