From 1c691e8626a11273b972d6a48b6334d1a1097eac Mon Sep 17 00:00:00 2001
From: xy <10816187@qq.com>
Date: Wed, 12 Mar 2025 16:34:54 +0800
Subject: [PATCH] feat: RBAC
---
admin/src/api/permission/index.ts | 10 +-
admin/src/pages/role/assignMenus.tsx | 2 -
admin/src/pages/role/assignResources.tsx | 126 ++++++++++++++++++
admin/src/pages/role/index.tsx | 20 ++-
server/src/common/guard/permission.guard.ts | 42 ++++--
.../permission/permission.controller.ts | 4 +-
.../system/permission/permission.service.ts | 3 +
.../modules/system/test/test.controller.ts | 2 -
server/src/utils/index.ts | 5 +
9 files changed, 188 insertions(+), 26 deletions(-)
create mode 100644 admin/src/pages/role/assignResources.tsx
diff --git a/admin/src/api/permission/index.ts b/admin/src/api/permission/index.ts
index 47664d0..30d8e8d 100644
--- a/admin/src/api/permission/index.ts
+++ b/admin/src/api/permission/index.ts
@@ -1,17 +1,17 @@
import request from '@/utils/request';
-export function assignMenus(id: string, menuIds: string[]) {
- return request.put(`/permission/roles/${id}/menus`, { menuIds });
+export function assignMenus(id: string, ids: string[]) {
+ return request.put(`/permission/roles/${id}/menus`, { ids });
}
export function getMenus(id: string) {
return request.get(`/permission/roles/${id}/menus`);
}
-export function assignResources(id: string, resourceIds: string[]) {
- return request.put(`/permission/roles/${id}/menus`, { resourceIds });
+export function assignResources(id: string, ids: string[]) {
+ return request.put(`/permission/roles/${id}/resources`, { ids });
}
export function getResources(id: string) {
- return request.get(`/permission/roles/${id}/menus`);
+ return request.get(`/permission/roles/${id}/resources`);
}
diff --git a/admin/src/pages/role/assignMenus.tsx b/admin/src/pages/role/assignMenus.tsx
index 4c58e06..6172816 100644
--- a/admin/src/pages/role/assignMenus.tsx
+++ b/admin/src/pages/role/assignMenus.tsx
@@ -3,9 +3,7 @@ import { Modal, Form, Input, Message, Tree } from '@arco-design/web-react';
import { getTreeMenu } from '@/api/menu';
import {
getMenus,
- getResources,
assignMenus,
- assignResources,
} from '@/api/permission';
interface Props {
diff --git a/admin/src/pages/role/assignResources.tsx b/admin/src/pages/role/assignResources.tsx
new file mode 100644
index 0000000..a24e9fd
--- /dev/null
+++ b/admin/src/pages/role/assignResources.tsx
@@ -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 (
+
+ 分配权限}
+ visible={visible}
+ onOk={onOk}
+ onCancel={handleCancel}
+ confirmLoading={confirmLoading}
+ >
+
+
+
+
+ {handleFinished && (
+ {
+ setCheckedKeys(value);
+ }}
+ treeData={treeData}
+ fieldNames={{
+ key: 'id',
+ title: 'name',
+ }}
+ >
+ )}
+
+
+
+
+ );
+}
+
+export default GrantPermission;
diff --git a/admin/src/pages/role/index.tsx b/admin/src/pages/role/index.tsx
index 38686ec..9e4a567 100644
--- a/admin/src/pages/role/index.tsx
+++ b/admin/src/pages/role/index.tsx
@@ -14,10 +14,10 @@ import Form from './form';
import {IconPlus} from '@arco-design/web-react/icon';
import {getColumns, TableOptions} from './constants';
import {getRole,deleteRole} from "@/api/role";
-import AssignMenusForm from "./grantPerm";
+import AssignMenusForm from "./assignMenus";
+import AssignResources from "./assignResources";
import SearchForm from "./searchForm";
-
const {Title} = Typography;
function UserManage() {
@@ -25,6 +25,7 @@ function UserManage() {
const [visible, setVisible] = useState(false);
const [visibleAssignMenus, setVisibleAssignMenus] = useState(false);
+ const [visibleAssignResources, setVisibleAssignResources] = useState(false);
const [record, setRecord] = useState(null);
const [pagination, setPagination] = useState({
sizeCanChange: true,
@@ -45,8 +46,9 @@ function UserManage() {
setVisibleAssignMenus(true);
};
- const showAssignResourcesModal = () => {
-
+ const showAssignResourcesModal = (payload) => {
+ setRecord(payload);
+ setVisibleAssignResources(true);
}
const showModal = (payload?) => {
@@ -116,6 +118,7 @@ function UserManage() {
setRecord(null);
setVisible(false);
setVisibleAssignMenus(false);
+ setVisibleAssignResources(false);
await refetch();
};
@@ -123,6 +126,7 @@ function UserManage() {
setRecord(null);
setVisible(false);
setVisibleAssignMenus(false);
+ setVisibleAssignResources(false);
};
useEffect(() => {
@@ -148,6 +152,14 @@ function UserManage() {
visible={visibleAssignMenus}
record={record}
/>
+
+
+
角色管理
diff --git a/server/src/common/guard/permission.guard.ts b/server/src/common/guard/permission.guard.ts
index 8101127..492a7ca 100644
--- a/server/src/common/guard/permission.guard.ts
+++ b/server/src/common/guard/permission.guard.ts
@@ -14,6 +14,7 @@ import { SKIP_AUTH } from '@/common/decorators/skip-auth.decorator';
import { PERMISSIONS_KEY } from '@/common/decorators/permissions.decorator';
import { PrismaService } from '@/prisma/prisma.service';
import { PermissionType, Role } from '@/common/enum';
+import { isArrayFullyContained } from '@/utils';
@Injectable()
export class PermissionGuard implements CanActivate {
@@ -44,6 +45,9 @@ export class PermissionGuard implements CanActivate {
sysUserRole: {
some: {
userId: request.user.sub,
+ role: {
+ status: 1,
+ },
},
},
},
@@ -59,18 +63,34 @@ export class PermissionGuard implements CanActivate {
return true;
}
- // // 已知 权限字符 通过权限字符找出权限id 再根据权限id 找出拥有权限的角色code
- // const hasPermission = await this.prisma.sysRolePermission.findMany({
- // where: {
- // roleId: {
- // in: roleIds,
- // },
- // },
- // });
- // // TODO
- //
- // console.log(hasPermission);
+ // 已知 权限字符 通过权限字符判断是否有符合条件的结果
+ const rolePermissions = await this.prisma.sysResource.findMany({
+ select: {
+ permissionKey: true,
+ },
+ where: {
+ type: PermissionType.Api,
+ sysResourcePermission: {
+ some: {
+ permission: {
+ sysRolePermission: {
+ some: {
+ roleId: {
+ in: roleIds,
+ },
+ },
+ },
+ },
+ },
+ },
+ },
+ });
+ const hasPermission = isArrayFullyContained(
+ rolePermissions.map((item) => item.permissionKey),
+ requiredPermissions,
+ );
+ if (hasPermission) return true;
// 判断当前接口是否存在权限
throw new ForbiddenException('无权访问!');
}
diff --git a/server/src/modules/system/permission/permission.controller.ts b/server/src/modules/system/permission/permission.controller.ts
index 4ab24fe..5ad9403 100644
--- a/server/src/modules/system/permission/permission.controller.ts
+++ b/server/src/modules/system/permission/permission.controller.ts
@@ -24,14 +24,14 @@ export class PermissionController {
@Get('/roles/:roleId/resources')
getRolResources(@Param('roleId') roleId: string) {
- return this.permissionService.getRoleResources(roleId, PermissionType.Menu);
+ return this.permissionService.getRoleResources(roleId, PermissionType.Api);
}
@Put('/roles/:roleId/resources')
assignResources(@Param('roleId') roleId: string, @Body() data: GrantMenuDto) {
return this.permissionService.assignResources(
roleId,
- PermissionType.Menu,
+ PermissionType.Api,
data,
);
}
diff --git a/server/src/modules/system/permission/permission.service.ts b/server/src/modules/system/permission/permission.service.ts
index 96edb66..6463816 100644
--- a/server/src/modules/system/permission/permission.service.ts
+++ b/server/src/modules/system/permission/permission.service.ts
@@ -13,6 +13,9 @@ export class PermissionService {
resourceId: true,
},
where: {
+ resource: {
+ type,
+ },
permission: {
sysRolePermission: {
some: { roleId },
diff --git a/server/src/modules/system/test/test.controller.ts b/server/src/modules/system/test/test.controller.ts
index 51b7376..5b0c2ae 100644
--- a/server/src/modules/system/test/test.controller.ts
+++ b/server/src/modules/system/test/test.controller.ts
@@ -1,11 +1,9 @@
import { Controller, Get } from '@nestjs/common';
import { TestService } from './test.service';
-import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
import { Permissions } from '@/common/decorators/permissions.decorator';
@Controller()
-// @SkipAuth()
@Permissions('test', 'test2')
export class TestController {
constructor(private readonly testService: TestService) {}
diff --git a/server/src/utils/index.ts b/server/src/utils/index.ts
index e69de29..d4bd419 100644
--- a/server/src/utils/index.ts
+++ b/server/src/utils/index.ts
@@ -0,0 +1,5 @@
+import { difference, isEmpty } from 'lodash';
+
+export const isArrayFullyContained = (arr1: any[], arr2: any[]) => {
+ return isEmpty(difference(arr2, arr1));
+};