feat: RBAC
This commit is contained in:
@@ -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`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
126
admin/src/pages/role/assignResources.tsx
Normal file
126
admin/src/pages/role/assignResources.tsx
Normal 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;
|
||||||
@@ -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}/>
|
||||||
|
|||||||
@@ -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('无权访问!');
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ export class PermissionService {
|
|||||||
resourceId: true,
|
resourceId: true,
|
||||||
},
|
},
|
||||||
where: {
|
where: {
|
||||||
|
resource: {
|
||||||
|
type,
|
||||||
|
},
|
||||||
permission: {
|
permission: {
|
||||||
sysRolePermission: {
|
sysRolePermission: {
|
||||||
some: { roleId },
|
some: { roleId },
|
||||||
|
|||||||
@@ -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) {}
|
||||||
|
|||||||
@@ -0,0 +1,5 @@
|
|||||||
|
import { difference, isEmpty } from 'lodash';
|
||||||
|
|
||||||
|
export const isArrayFullyContained = (arr1: any[], arr2: any[]) => {
|
||||||
|
return isEmpty(difference(arr2, arr1));
|
||||||
|
};
|
||||||
|
|||||||
Reference in New Issue
Block a user