feat: RBAC

This commit is contained in:
2025-03-12 11:38:34 +08:00
parent e06d00feff
commit fb3686968d
17 changed files with 144 additions and 98 deletions

View File

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

View File

@@ -12,8 +12,6 @@ export const enum TableOptions {
DELETE = 'delete', DELETE = 'delete',
} }
export function getColumns(callback: (type: TableOptions, record: any) => Promise<void>): TableColumnProps[] { export function getColumns(callback: (type: TableOptions, record: any) => Promise<void>): TableColumnProps[] {
return [ return [
{ {
@@ -28,7 +26,7 @@ export function getColumns(callback: (type: TableOptions, record: any) => Promis
}, },
{ {
title: '权限字符', title: '权限字符',
dataIndex: 'permission', dataIndex: 'permissionKey',
align: 'center', align: 'center',
}, },
{ {

View File

@@ -88,7 +88,7 @@ export default function FormComponent(props: Props) {
<Form.Item label="权限名称" field="name" rules={[{required: true}]}> <Form.Item label="权限名称" field="name" rules={[{required: true}]}>
<Input placeholder="请输入权限名称"/> <Input placeholder="请输入权限名称"/>
</Form.Item> </Form.Item>
<Form.Item label="权限字符" field="permission" rules={[{required: true}]}> <Form.Item label="权限字符" field="permissionKey" rules={[{required: true}]}>
<Input placeholder="请输入权限字符"/> <Input placeholder="请输入权限字符"/>
</Form.Item> </Form.Item>
<Form.Item label="排序" field="sort"> <Form.Item label="排序" field="sort">

View File

@@ -1,13 +1,18 @@
import React, { useEffect, useState } from 'react'; import React, { useEffect, useState } from 'react';
import { Modal, Form, Input, Message, Tree } from '@arco-design/web-react'; import { Modal, Form, Input, Message, Tree } from '@arco-design/web-react';
import { getTreeMenu } from '@/api/menu'; import { getTreeMenu } from '@/api/menu';
import { getRoleMenuIds, grantPermission } from '@/api/permission'; import {
getMenus,
getResources,
assignMenus,
assignResources,
} from '@/api/permission';
interface Props { interface Props {
record: { [key: string]: any } | null, record: { [key: string]: any } | null;
visible: boolean, visible: boolean;
handleConfirm: () => void, handleConfirm: () => void;
handleCancel: () => void handleCancel: () => void;
} }
function GrantPermission(props: Props) { function GrantPermission(props: Props) {
@@ -20,7 +25,7 @@ function GrantPermission(props: Props) {
try { try {
setConfirmLoading(true); setConfirmLoading(true);
const values = await form.validate(); const values = await form.validate();
await grantPermission(values.id, checkedKeys); await assignMenus(values.id, checkedKeys);
Message.success(`操作成功!`); Message.success(`操作成功!`);
handleConfirm(); handleConfirm();
} catch (e) { } catch (e) {
@@ -33,12 +38,12 @@ function GrantPermission(props: Props) {
const [form] = Form.useForm(); const [form] = Form.useForm();
useEffect(() => { useEffect(() => {
record && form.setFieldsValue({ record &&
...record form.setFieldsValue({
...record,
}); });
}, [record]); }, [record]);
const [handleFinished, setHandleFinished] = useState(false); const [handleFinished, setHandleFinished] = useState(false);
const handleMenuAndKeys = (menu, menuIds) => { const handleMenuAndKeys = (menu, menuIds) => {
let newMenuIds = menuIds.map((menuId) => menuId); let newMenuIds = menuIds.map((menuId) => menuId);
@@ -49,8 +54,8 @@ function GrantPermission(props: Props) {
item.parentId = item.id; item.parentId = item.id;
if (item.children?.length > 0) { if (item.children?.length > 0) {
if (item.children.some(child => !newMenuIds.includes(child.id))) { if (item.children.some((child) => !newMenuIds.includes(child.id))) {
newMenuIds = newMenuIds.filter(id => id !== item.id); newMenuIds = newMenuIds.filter((id) => id !== item.id);
} }
handleMenu(item.children); handleMenu(item.children);
} }
@@ -72,7 +77,7 @@ function GrantPermission(props: Props) {
if (visible) { if (visible) {
const fetchTreeMenu = async () => { const fetchTreeMenu = async () => {
const { data } = await getTreeMenu(); const { data } = await getTreeMenu();
const { data: menuIds } = await getRoleMenuIds(record.id); const { data: menuIds } = await getMenus(record.id);
handleMenuAndKeys(data, menuIds); handleMenuAndKeys(data, menuIds);
}; };
fetchTreeMenu(); fetchTreeMenu();
@@ -98,8 +103,8 @@ function GrantPermission(props: Props) {
<Input /> <Input />
</Form.Item> </Form.Item>
<Form.Item label="菜单权限"> <Form.Item label="菜单权限">
{ {handleFinished && (
handleFinished && <Tree <Tree
checkable checkable
checkedKeys={checkedKeys} checkedKeys={checkedKeys}
autoExpandParent={true} autoExpandParent={true}
@@ -109,10 +114,10 @@ function GrantPermission(props: Props) {
treeData={treeData} treeData={treeData}
fieldNames={{ fieldNames={{
key: 'id', key: 'id',
title: 'name' title: 'name',
}} }}
></Tree> ></Tree>
} )}
</Form.Item> </Form.Item>
</Form> </Form>
</Modal> </Modal>

View File

@@ -14,7 +14,7 @@ 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 GrantRolesForm from "./grantPerm"; import AssignMenusForm from "./grantPerm";
import SearchForm from "./searchForm"; import SearchForm from "./searchForm";
@@ -24,7 +24,7 @@ function UserManage() {
const reactQueryKey = useHistory().location.pathname const reactQueryKey = useHistory().location.pathname
const [visible, setVisible] = useState(false); const [visible, setVisible] = useState(false);
const [visibleAssignRoles, setVisibleAssignRoles] = useState(false); const [visibleAssignMenus, setVisibleAssignMenus] = 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,
@@ -40,11 +40,15 @@ function UserManage() {
pageSize: pagination.pageSize, pageSize: pagination.pageSize,
}); });
const showAssignRolesModal = (payload) => { const showAssignMenusModal = (payload) => {
setRecord(payload); setRecord(payload);
setVisibleAssignRoles(true); setVisibleAssignMenus(true);
}; };
const showAssignResourcesModal = () => {
}
const showModal = (payload?) => { const showModal = (payload?) => {
payload && setRecord(payload); payload && setRecord(payload);
setVisible(true); setVisible(true);
@@ -65,7 +69,10 @@ function UserManage() {
await onDelete(record); await onDelete(record);
break break
case TableOptions.ASSIGN_MENUS: case TableOptions.ASSIGN_MENUS:
showAssignRolesModal(record); showAssignMenusModal(record);
break
case TableOptions.ASSIGN_RESOURCES:
showAssignResourcesModal(record);
break break
} }
} }
@@ -108,14 +115,14 @@ function UserManage() {
const handleConfirm = async () => { const handleConfirm = async () => {
setRecord(null); setRecord(null);
setVisible(false); setVisible(false);
setVisibleAssignRoles(false); setVisibleAssignMenus(false);
await refetch(); await refetch();
}; };
const handleCancel = () => { const handleCancel = () => {
setRecord(null); setRecord(null);
setVisible(false); setVisible(false);
setVisibleAssignRoles(false); setVisibleAssignMenus(false);
}; };
useEffect(() => { useEffect(() => {
@@ -135,10 +142,10 @@ function UserManage() {
visible={visible} visible={visible}
record={record} record={record}
/> />
<GrantRolesForm <AssignMenusForm
handleConfirm={handleConfirm} handleConfirm={handleConfirm}
handleCancel={handleCancel} handleCancel={handleCancel}
visible={visibleAssignRoles} visible={visibleAssignMenus}
record={record} record={record}
/> />
<Title heading={6}></Title> <Title heading={6}></Title>

View File

@@ -1,14 +0,0 @@
/*
Warnings:
- A unique constraint covering the columns `[permission]` on the table `sys_resource` will be added. If there are existing duplicate values, this will fail.
*/
-- DropIndex
DROP INDEX "sys_resource_name_type_key";
-- AlterTable
ALTER TABLE "sys_resource" ADD COLUMN "permission" TEXT;
-- CreateIndex
CREATE UNIQUE INDEX "sys_resource_permission_key" ON "sys_resource"("permission");

View File

@@ -80,6 +80,7 @@ CREATE TABLE "sys_resource" (
"updated_at" TIMESTAMP(3) NOT NULL, "updated_at" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"path" TEXT, "path" TEXT,
"permission_key" TEXT,
"parent_id" TEXT, "parent_id" TEXT,
"sort" INTEGER NOT NULL DEFAULT 0, "sort" INTEGER NOT NULL DEFAULT 0,
"type" INTEGER NOT NULL, "type" INTEGER NOT NULL,
@@ -135,10 +136,10 @@ CREATE INDEX "sys_role_permission_role_id_idx" ON "sys_role_permission"("role_id
CREATE INDEX "sys_role_permission_permission_id_idx" ON "sys_role_permission"("permission_id"); CREATE INDEX "sys_role_permission_permission_id_idx" ON "sys_role_permission"("permission_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "sys_resource_name_type_key" ON "sys_resource"("name", "type"); CREATE UNIQUE INDEX "sys_resource_path_key" ON "sys_resource"("path");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "sys_resource_path_key" ON "sys_resource"("path"); CREATE UNIQUE INDEX "sys_resource_permission_key_key" ON "sys_resource"("permission_key");
-- CreateIndex -- CreateIndex
CREATE INDEX "sys_resource_permission_resource_id_idx" ON "sys_resource_permission"("resource_id"); CREATE INDEX "sys_resource_permission_resource_id_idx" ON "sys_resource_permission"("resource_id");

View File

@@ -109,7 +109,7 @@ model SysResource {
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间 updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
name String /// 菜单名称或者接口名称 name String /// 菜单名称或者接口名称
path String? @unique() /// 菜单或api路径 path String? @unique() /// 菜单或api路径
permission String? @unique() /// 权限字符 permissionKey String? @unique() @map("permission_key") /// 权限字符
parentId String? @map("parent_id") parentId String? @map("parent_id")
sort Int @default(0) sort Int @default(0)
type Int /// 资源类型 0:菜单 1:api type Int /// 资源类型 0:菜单 1:api

View File

@@ -7,27 +7,30 @@ enum PermissionType {
} }
async function main() { async function main() {
await prisma.sysUser.upsert({
where: { account: 'admin' },
update: {},
create: {
name: '管理员',
account: 'admin',
password: '$2a$10$rWb52cHK1hrWuQQg3LiOJ.crDfIqjoOIoglGM96a.Ao1DL/dQwKCu',
passwordSalt: '$2a$10$rWb52cHK1hrWuQQg3LiOJ.',
status: 1,
},
});
const menus = [ const menus = [
{ name: '用户管理', path: 'user', type: PermissionType.Menu }, { name: '用户管理', path: 'user', type: PermissionType.Menu },
{ name: '角色管理', path: 'role', type: PermissionType.Menu }, { name: '角色管理', path: 'role', type: PermissionType.Menu },
{ name: '菜单管理', path: 'menu', type: PermissionType.Menu }, { name: '菜单管理', path: 'menu', type: PermissionType.Menu },
{ name: '系统配置', path: 'config', type: PermissionType.Menu }, { name: '系统配置', path: 'config', type: PermissionType.Menu },
{ name: '资源管理', path: 'resource', type: PermissionType.Menu },
]; ];
for (const menu of menus) {
// 创建菜单
await prisma.$transaction(async (_prisma) => { await prisma.$transaction(async (_prisma) => {
const user = await _prisma.sysUser.upsert({
where: { account: 'admin' },
update: {},
create: {
name: '管理员',
account: 'admin',
password:
'$2a$10$rWb52cHK1hrWuQQg3LiOJ.crDfIqjoOIoglGM96a.Ao1DL/dQwKCu',
passwordSalt: '$2a$10$rWb52cHK1hrWuQQg3LiOJ.',
status: 1,
gender: 0,
},
});
for (const menu of menus) {
const { id: resourceId } = await _prisma.sysResource.create({ const { id: resourceId } = await _prisma.sysResource.create({
data: menu, data: menu,
}); });
@@ -39,8 +42,24 @@ async function main() {
await _prisma.sysResourcePermission.create({ await _prisma.sysResourcePermission.create({
data: { resourceId, permissionId }, data: { resourceId, permissionId },
}); });
});
} }
// 创建角色并关联
const role = await _prisma.sysRole.create({
data: {
name: '管理员',
code: 'admin',
status: 1,
},
});
await _prisma.sysUserRole.create({
data: {
userId: user.id,
roleId: role.id,
},
});
});
} }
main() main()

View File

@@ -52,25 +52,24 @@ export class PermissionGuard implements CanActivate {
// 所有用户的code // 所有用户的code
const roleCodes = userRoles.map((item) => item.code); const roleCodes = userRoles.map((item) => item.code);
const roleIds = userRoles.map((item) => item.id); const roleIds = userRoles.map((item) => item.id);
// 如果是管理员 直接通过验证不需要进行接口鉴权 // 如果是管理员 直接通过验证不需要进行接口鉴权
if (roleCodes.includes(Role.Admin)) { if (roleCodes.includes(Role.Admin)) {
request['isAdmin'] = true; request['isAdmin'] = true;
return true; return true;
} }
console.log(roleIds); // // 已知 权限字符 通过权限字符找出权限id 再根据权限id 找出拥有权限的角色code
// const hasPermission = await this.prisma.sysRolePermission.findMany({
// 已知 权限字符 通过权限字符找出权限id 再根据权限id 找出拥有权限的角色code // where: {
const hasPermission = await this.prisma.sysRolePermission.findMany({ // roleId: {
where: { // in: roleIds,
roleId: { // },
in: roleIds, // },
}, // });
}, // // TODO
}); //
// TODO // console.log(hasPermission);
console.log(hasPermission);
// 判断当前接口是否存在权限 // 判断当前接口是否存在权限
throw new ForbiddenException('无权访问!'); throw new ForbiddenException('无权访问!');

View File

@@ -67,11 +67,9 @@ export class MenuService {
async update(id: string, data: UpdateMenuDto) { async update(id: string, data: UpdateMenuDto) {
const doesExists = await this.prisma.sysResource.findUnique({ const doesExists = await this.prisma.sysResource.findUnique({
where: { path: data.path }, where: { path: data.path, NOT: { id } },
}); });
console.log(doesExists);
if (doesExists) { if (doesExists) {
throw new HttpException('菜单路径不可重复!', HttpStatus.NOT_FOUND); throw new HttpException('菜单路径不可重复!', HttpStatus.NOT_FOUND);
} }

View File

@@ -2,5 +2,5 @@ import { IsString } from 'class-validator';
export class GrantMenuDto { export class GrantMenuDto {
@IsString({ each: true }) @IsString({ each: true })
menuIds: string[]; ids: string[];
} }

View File

@@ -2,18 +2,37 @@ import { Controller, Body, Put, Param, Get } from '@nestjs/common';
import { PermissionService } from './permission.service'; import { PermissionService } from './permission.service';
import { GrantMenuDto } from './dto/grant-menu.dto'; import { GrantMenuDto } from './dto/grant-menu.dto';
import { PermissionType } from '@/common/enum';
@Controller() @Controller()
export class PermissionController { export class PermissionController {
constructor(private readonly permissionService: PermissionService) {} constructor(private readonly permissionService: PermissionService) {}
@Get('/roles/:roleId/resources') @Get('/roles/:roleId/menus')
getRoleMenus(@Param('roleId') roleId: string) { getRoleMenus(@Param('roleId') roleId: string) {
return this.permissionService.getRoleMenus(roleId); return this.permissionService.getRoleResources(roleId, PermissionType.Menu);
}
@Put('/roles/:roleId/menus')
assignMenus(@Param('roleId') roleId: string, @Body() data: GrantMenuDto) {
return this.permissionService.assignResources(
roleId,
PermissionType.Menu,
data,
);
}
@Get('/roles/:roleId/resources')
getRolResources(@Param('roleId') roleId: string) {
return this.permissionService.getRoleResources(roleId, PermissionType.Menu);
} }
@Put('/roles/:roleId/resources') @Put('/roles/:roleId/resources')
assignMenus(@Param('roleId') roleId: string, @Body() dto: GrantMenuDto) { assignResources(@Param('roleId') roleId: string, @Body() data: GrantMenuDto) {
return this.permissionService.assignMenus(roleId, dto); return this.permissionService.assignResources(
roleId,
PermissionType.Menu,
data,
);
} }
} }

View File

@@ -1,13 +1,14 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { GrantMenuDto } from '@/modules/system/permission/dto/grant-menu.dto'; import { GrantMenuDto } from '@/modules/system/permission/dto/grant-menu.dto';
import { PrismaService } from '@/prisma/prisma.service'; import { PrismaService } from '@/prisma/prisma.service';
import { PermissionType } from '@/common/enum';
@Injectable() @Injectable()
export class PermissionService { export class PermissionService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
async getRoleMenus(roleId: string) { async getRoleResources(roleId: string, type: PermissionType) {
const menuIds = await this.prisma.sysResourcePermission.findMany({ const ids = await this.prisma.sysResourcePermission.findMany({
select: { select: {
resourceId: true, resourceId: true,
}, },
@@ -20,19 +21,25 @@ export class PermissionService {
}, },
}); });
return menuIds.map((item) => item.resourceId); return ids.map((item) => item.resourceId);
} }
async assignMenus(roleId: string, { menuIds }: GrantMenuDto) { async assignResources(
roleId: string,
type: PermissionType,
{ ids }: GrantMenuDto,
) {
await this.prisma.$transaction(async (prisma) => { await this.prisma.$transaction(async (prisma) => {
// 删除角色对应的权限 // 删除角色对应的权限
await prisma.sysRolePermission.deleteMany({ await prisma.sysRolePermission.deleteMany({
where: { roleId }, where: {
roleId,
},
}); });
// 通过菜单id 找出所有权限id // 通过资源id 找出所有权限id
const rolePermIds = await prisma.sysResourcePermission.findMany({ const rolePermIds = await prisma.sysResourcePermission.findMany({
select: { permissionId: true }, select: { permissionId: true },
where: { resourceId: { in: menuIds } }, where: { resourceId: { in: ids } },
}); });
// 根据权限id 关联菜单和权限 // 根据权限id 关联菜单和权限
if (rolePermIds.length > 0) { if (rolePermIds.length > 0) {

View File

@@ -8,7 +8,7 @@ export class CreateResourceDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
permission: string; permissionKey: string;
@IsOptional() @IsOptional()
@IsString() @IsString()

View File

@@ -11,7 +11,7 @@ export class ResourceService {
async create(data: CreateResourceDto) { async create(data: CreateResourceDto) {
const doesExists = await this.prisma.sysResource.findUnique({ const doesExists = await this.prisma.sysResource.findUnique({
where: { path: data.permission }, where: { permissionKey: data.permissionKey },
}); });
if (doesExists) if (doesExists)
@@ -66,7 +66,7 @@ export class ResourceService {
async update(id: string, data: UpdateResourceDto) { async update(id: string, data: UpdateResourceDto) {
const doesExists = await this.prisma.sysResource.findUnique({ const doesExists = await this.prisma.sysResource.findUnique({
where: { path: data.permission, NOT: { id } }, where: { permissionKey: data.permissionKey, NOT: { id } },
}); });
if (doesExists) if (doesExists)

View File

@@ -33,7 +33,6 @@ export class UserController {
getUserInfo(@Request() req) { getUserInfo(@Request() req) {
const { sub } = req['user']; const { sub } = req['user'];
const { isAdmin } = req; const { isAdmin } = req;
console.log(isAdmin);
return this.userService.getUserInfo(sub, isAdmin); return this.userService.getUserInfo(sub, isAdmin);
} }