feat: RBAC

This commit is contained in:
2025-03-12 16:34:54 +08:00
parent fb3686968d
commit 1c691e8626
9 changed files with 188 additions and 26 deletions

View File

@@ -1,17 +1,17 @@
import request from '@/utils/request'; import request from '@/utils/request';
export function assignMenus(id: string, menuIds: string[]) { export function assignMenus(id: string, ids: string[]) {
return request.put(`/permission/roles/${id}/menus`, { menuIds }); return request.put(`/permission/roles/${id}/menus`, { ids });
} }
export function getMenus(id: string) { export function getMenus(id: string) {
return request.get(`/permission/roles/${id}/menus`); return request.get(`/permission/roles/${id}/menus`);
} }
export function assignResources(id: string, resourceIds: string[]) { export function assignResources(id: string, ids: string[]) {
return request.put(`/permission/roles/${id}/menus`, { resourceIds }); return request.put(`/permission/roles/${id}/resources`, { ids });
} }
export function getResources(id: string) { export function getResources(id: string) {
return request.get(`/permission/roles/${id}/menus`); return request.get(`/permission/roles/${id}/resources`);
} }

View File

@@ -3,9 +3,7 @@ import { Modal, Form, Input, Message, Tree } from '@arco-design/web-react';
import { getTreeMenu } from '@/api/menu'; import { getTreeMenu } from '@/api/menu';
import { import {
getMenus, getMenus,
getResources,
assignMenus, assignMenus,
assignResources,
} from '@/api/permission'; } from '@/api/permission';
interface Props { interface Props {

View File

@@ -0,0 +1,126 @@
import React, { useEffect, useState } from 'react';
import { Modal, Form, Input, Message, Tree } from '@arco-design/web-react';
import { getTreeResource } from '@/api/resource';
import {
getResources,
assignResources,
} from '@/api/permission';
interface Props {
record: { [key: string]: any } | null;
visible: boolean;
handleConfirm: () => void;
handleCancel: () => void;
}
function GrantPermission(props: Props) {
const { visible, record, handleConfirm, handleCancel } = props;
const [confirmLoading, setConfirmLoading] = useState(false);
const [checkedKeys, setCheckedKeys] = useState([]);
const [treeData, setTreeData] = useState([]);
const onOk = async () => {
try {
setConfirmLoading(true);
const values = await form.validate();
await assignResources(values.id, checkedKeys);
Message.success(`操作成功!`);
handleConfirm();
} catch (e) {
console.log(e);
} finally {
setConfirmLoading(false);
}
};
const [form] = Form.useForm();
useEffect(() => {
record &&
form.setFieldsValue({
...record,
});
}, [record]);
const [handleFinished, setHandleFinished] = useState(false);
const handleMenuAndKeys = (menu, menuIds) => {
let newMenuIds = menuIds.map((menuId) => menuId);
function handleMenu(_menu) {
_menu.forEach((item) => {
// 组件需要的key必须为string
item.parentId = item.id;
if (item.children?.length > 0) {
if (item.children.some((child) => !newMenuIds.includes(child.id))) {
newMenuIds = newMenuIds.filter((id) => id !== item.id);
}
handleMenu(item.children);
}
});
}
handleMenu(menu);
setTreeData(menu);
setCheckedKeys(newMenuIds);
};
useEffect(() => {
if (treeData.length) {
setHandleFinished(true);
}
}, [treeData]);
useEffect(() => {
if (visible) {
const fetchTreeMenu = async () => {
const { data } = await getTreeResource();
const { data: menuIds } = await getResources(record.id);
handleMenuAndKeys(data, menuIds);
};
fetchTreeMenu();
} else {
form.resetFields();
setTreeData([]);
setCheckedKeys([]);
setHandleFinished(false);
}
}, [visible]);
return (
<div>
<Modal
title={<span></span>}
visible={visible}
onOk={onOk}
onCancel={handleCancel}
confirmLoading={confirmLoading}
>
<Form form={form}>
<Form.Item hidden field="id">
<Input />
</Form.Item>
<Form.Item label="菜单权限">
{handleFinished && (
<Tree
checkable
checkedKeys={checkedKeys}
autoExpandParent={true}
onCheck={(value) => {
setCheckedKeys(value);
}}
treeData={treeData}
fieldNames={{
key: 'id',
title: 'name',
}}
></Tree>
)}
</Form.Item>
</Form>
</Modal>
</div>
);
}
export default GrantPermission;

View File

@@ -14,10 +14,10 @@ import Form from './form';
import {IconPlus} from '@arco-design/web-react/icon'; import {IconPlus} from '@arco-design/web-react/icon';
import {getColumns, TableOptions} from './constants'; import {getColumns, TableOptions} from './constants';
import {getRole,deleteRole} from "@/api/role"; import {getRole,deleteRole} from "@/api/role";
import AssignMenusForm from "./grantPerm"; import AssignMenusForm from "./assignMenus";
import AssignResources from "./assignResources";
import SearchForm from "./searchForm"; import SearchForm from "./searchForm";
const {Title} = Typography; const {Title} = Typography;
function UserManage() { function UserManage() {
@@ -25,6 +25,7 @@ function UserManage() {
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [visibleAssignMenus, setVisibleAssignMenus] = useState(false); const [visibleAssignMenus, setVisibleAssignMenus] = useState(false);
const [visibleAssignResources, setVisibleAssignResources] = useState(false);
const [record, setRecord] = useState(null); const [record, setRecord] = useState(null);
const [pagination, setPagination] = useState<PaginationProps>({ const [pagination, setPagination] = useState<PaginationProps>({
sizeCanChange: true, sizeCanChange: true,
@@ -45,8 +46,9 @@ function UserManage() {
setVisibleAssignMenus(true); setVisibleAssignMenus(true);
}; };
const showAssignResourcesModal = () => { const showAssignResourcesModal = (payload) => {
setRecord(payload);
setVisibleAssignResources(true);
} }
const showModal = (payload?) => { const showModal = (payload?) => {
@@ -116,6 +118,7 @@ function UserManage() {
setRecord(null); setRecord(null);
setVisible(false); setVisible(false);
setVisibleAssignMenus(false); setVisibleAssignMenus(false);
setVisibleAssignResources(false);
await refetch(); await refetch();
}; };
@@ -123,6 +126,7 @@ function UserManage() {
setRecord(null); setRecord(null);
setVisible(false); setVisible(false);
setVisibleAssignMenus(false); setVisibleAssignMenus(false);
setVisibleAssignResources(false);
}; };
useEffect(() => { useEffect(() => {
@@ -148,6 +152,14 @@ function UserManage() {
visible={visibleAssignMenus} visible={visibleAssignMenus}
record={record} record={record}
/> />
<AssignResources
handleConfirm={handleConfirm}
handleCancel={handleCancel}
visible={visibleAssignResources}
record={record}
/>
<Title heading={6}></Title> <Title heading={6}></Title>
<SearchForm onSearch={handleSearch}/> <SearchForm onSearch={handleSearch}/>

View File

@@ -14,6 +14,7 @@ import { SKIP_AUTH } from '@/common/decorators/skip-auth.decorator';
import { PERMISSIONS_KEY } from '@/common/decorators/permissions.decorator'; import { PERMISSIONS_KEY } from '@/common/decorators/permissions.decorator';
import { PrismaService } from '@/prisma/prisma.service'; import { PrismaService } from '@/prisma/prisma.service';
import { PermissionType, Role } from '@/common/enum'; import { PermissionType, Role } from '@/common/enum';
import { isArrayFullyContained } from '@/utils';
@Injectable() @Injectable()
export class PermissionGuard implements CanActivate { export class PermissionGuard implements CanActivate {
@@ -44,6 +45,9 @@ export class PermissionGuard implements CanActivate {
sysUserRole: { sysUserRole: {
some: { some: {
userId: request.user.sub, userId: request.user.sub,
role: {
status: 1,
},
}, },
}, },
}, },
@@ -59,18 +63,34 @@ export class PermissionGuard implements CanActivate {
return true; return true;
} }
// // 已知 权限字符 通过权限字符找出权限id 再根据权限id 找出拥有权限的角色code // 已知 权限字符 通过权限字符判断是否有符合条件的结果
// const hasPermission = await this.prisma.sysRolePermission.findMany({ const rolePermissions = await this.prisma.sysResource.findMany({
// where: { select: {
// roleId: { permissionKey: true,
// in: roleIds, },
// }, where: {
// }, type: PermissionType.Api,
// }); sysResourcePermission: {
// // TODO some: {
// permission: {
// console.log(hasPermission); sysRolePermission: {
some: {
roleId: {
in: roleIds,
},
},
},
},
},
},
},
});
const hasPermission = isArrayFullyContained(
rolePermissions.map((item) => item.permissionKey),
requiredPermissions,
);
if (hasPermission) return true;
// 判断当前接口是否存在权限 // 判断当前接口是否存在权限
throw new ForbiddenException('无权访问!'); throw new ForbiddenException('无权访问!');
} }

View File

@@ -24,14 +24,14 @@ export class PermissionController {
@Get('/roles/:roleId/resources') @Get('/roles/:roleId/resources')
getRolResources(@Param('roleId') roleId: string) { getRolResources(@Param('roleId') roleId: string) {
return this.permissionService.getRoleResources(roleId, PermissionType.Menu); return this.permissionService.getRoleResources(roleId, PermissionType.Api);
} }
@Put('/roles/:roleId/resources') @Put('/roles/:roleId/resources')
assignResources(@Param('roleId') roleId: string, @Body() data: GrantMenuDto) { assignResources(@Param('roleId') roleId: string, @Body() data: GrantMenuDto) {
return this.permissionService.assignResources( return this.permissionService.assignResources(
roleId, roleId,
PermissionType.Menu, PermissionType.Api,
data, data,
); );
} }

View File

@@ -13,6 +13,9 @@ export class PermissionService {
resourceId: true, resourceId: true,
}, },
where: { where: {
resource: {
type,
},
permission: { permission: {
sysRolePermission: { sysRolePermission: {
some: { roleId }, some: { roleId },

View File

@@ -1,11 +1,9 @@
import { Controller, Get } from '@nestjs/common'; import { Controller, Get } from '@nestjs/common';
import { TestService } from './test.service'; import { TestService } from './test.service';
import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
import { Permissions } from '@/common/decorators/permissions.decorator'; import { Permissions } from '@/common/decorators/permissions.decorator';
@Controller() @Controller()
// @SkipAuth()
@Permissions('test', 'test2') @Permissions('test', 'test2')
export class TestController { export class TestController {
constructor(private readonly testService: TestService) {} constructor(private readonly testService: TestService) {}

View File

@@ -0,0 +1,5 @@
import { difference, isEmpty } from 'lodash';
export const isArrayFullyContained = (arr1: any[], arr2: any[]) => {
return isEmpty(difference(arr2, arr1));
};