From fb3686968d3be734dafd3dbc3d0189af483180eb Mon Sep 17 00:00:00 2001 From: xy <10816187@qq.com> Date: Wed, 12 Mar 2025 11:38:34 +0800 Subject: [PATCH] feat: RBAC --- admin/src/api/permission/index.ts | 16 ++++-- admin/src/pages/resource/constants.tsx | 4 +- admin/src/pages/resource/form.tsx | 2 +- .../role/{grantPerm.tsx => assignMenus.tsx} | 39 ++++++++------- admin/src/pages/role/index.tsx | 25 ++++++---- .../migrations/20250311054029_/migration.sql | 14 ------ .../migration.sql | 5 +- server/prisma/schema.prisma | 2 +- server/prisma/seed.ts | 49 +++++++++++++------ server/src/common/guard/permission.guard.ts | 25 +++++----- .../src/modules/system/menu/menu.service.ts | 4 +- .../system/permission/dto/grant-menu.dto.ts | 2 +- .../permission/permission.controller.ts | 27 ++++++++-- .../system/permission/permission.service.ts | 21 +++++--- .../resource/dto/create-resource.dto.ts | 2 +- .../system/resource/resource.service.ts | 4 +- .../modules/system/user/user.controller.ts | 1 - 17 files changed, 144 insertions(+), 98 deletions(-) rename admin/src/pages/role/{grantPerm.tsx => assignMenus.tsx} (79%) delete mode 100644 server/prisma/migrations/20250311054029_/migration.sql rename server/prisma/migrations/{20250310150829_ => 20250312021820_}/migration.sql (97%) diff --git a/admin/src/api/permission/index.ts b/admin/src/api/permission/index.ts index 12a302c..47664d0 100644 --- a/admin/src/api/permission/index.ts +++ b/admin/src/api/permission/index.ts @@ -1,9 +1,17 @@ import request from '@/utils/request'; -export function grantPermission(id: string, menuIds: string[]) { - return request.put(`/permission/roles/${id}/resources`, { menuIds }); +export function assignMenus(id: string, menuIds: string[]) { + return request.put(`/permission/roles/${id}/menus`, { menuIds }); } -export function getRoleMenuIds(id: string) { - return request.get(`/permission/roles/${id}/resources`); +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 getResources(id: string) { + return request.get(`/permission/roles/${id}/menus`); } diff --git a/admin/src/pages/resource/constants.tsx b/admin/src/pages/resource/constants.tsx index 8dca381..9238c65 100644 --- a/admin/src/pages/resource/constants.tsx +++ b/admin/src/pages/resource/constants.tsx @@ -12,8 +12,6 @@ export const enum TableOptions { DELETE = 'delete', } - - export function getColumns(callback: (type: TableOptions, record: any) => Promise): TableColumnProps[] { return [ { @@ -28,7 +26,7 @@ export function getColumns(callback: (type: TableOptions, record: any) => Promis }, { title: '权限字符', - dataIndex: 'permission', + dataIndex: 'permissionKey', align: 'center', }, { diff --git a/admin/src/pages/resource/form.tsx b/admin/src/pages/resource/form.tsx index 1038f28..c8df5ab 100644 --- a/admin/src/pages/resource/form.tsx +++ b/admin/src/pages/resource/form.tsx @@ -88,7 +88,7 @@ export default function FormComponent(props: Props) { - + diff --git a/admin/src/pages/role/grantPerm.tsx b/admin/src/pages/role/assignMenus.tsx similarity index 79% rename from admin/src/pages/role/grantPerm.tsx rename to admin/src/pages/role/assignMenus.tsx index d2d5721..4c58e06 100644 --- a/admin/src/pages/role/grantPerm.tsx +++ b/admin/src/pages/role/assignMenus.tsx @@ -1,13 +1,18 @@ import React, { useEffect, useState } from 'react'; import { Modal, Form, Input, Message, Tree } from '@arco-design/web-react'; import { getTreeMenu } from '@/api/menu'; -import { getRoleMenuIds, grantPermission } from '@/api/permission'; +import { + getMenus, + getResources, + assignMenus, + assignResources, +} from '@/api/permission'; interface Props { - record: { [key: string]: any } | null, - visible: boolean, - handleConfirm: () => void, - handleCancel: () => void + record: { [key: string]: any } | null; + visible: boolean; + handleConfirm: () => void; + handleCancel: () => void; } function GrantPermission(props: Props) { @@ -20,7 +25,7 @@ function GrantPermission(props: Props) { try { setConfirmLoading(true); const values = await form.validate(); - await grantPermission(values.id, checkedKeys); + await assignMenus(values.id, checkedKeys); Message.success(`操作成功!`); handleConfirm(); } catch (e) { @@ -33,12 +38,12 @@ function GrantPermission(props: Props) { const [form] = Form.useForm(); useEffect(() => { - record && form.setFieldsValue({ - ...record - }); + record && + form.setFieldsValue({ + ...record, + }); }, [record]); - const [handleFinished, setHandleFinished] = useState(false); const handleMenuAndKeys = (menu, menuIds) => { let newMenuIds = menuIds.map((menuId) => menuId); @@ -49,8 +54,8 @@ function GrantPermission(props: Props) { 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); + if (item.children.some((child) => !newMenuIds.includes(child.id))) { + newMenuIds = newMenuIds.filter((id) => id !== item.id); } handleMenu(item.children); } @@ -72,7 +77,7 @@ function GrantPermission(props: Props) { if (visible) { const fetchTreeMenu = async () => { const { data } = await getTreeMenu(); - const { data: menuIds } = await getRoleMenuIds(record.id); + const { data: menuIds } = await getMenus(record.id); handleMenuAndKeys(data, menuIds); }; fetchTreeMenu(); @@ -98,8 +103,8 @@ function GrantPermission(props: Props) { - { - handleFinished && - } + )} diff --git a/admin/src/pages/role/index.tsx b/admin/src/pages/role/index.tsx index b604608..38686ec 100644 --- a/admin/src/pages/role/index.tsx +++ b/admin/src/pages/role/index.tsx @@ -14,7 +14,7 @@ import Form from './form'; import {IconPlus} from '@arco-design/web-react/icon'; import {getColumns, TableOptions} from './constants'; import {getRole,deleteRole} from "@/api/role"; -import GrantRolesForm from "./grantPerm"; +import AssignMenusForm from "./grantPerm"; import SearchForm from "./searchForm"; @@ -24,7 +24,7 @@ function UserManage() { const reactQueryKey = useHistory().location.pathname const [visible, setVisible] = useState(false); - const [visibleAssignRoles, setVisibleAssignRoles] = useState(false); + const [visibleAssignMenus, setVisibleAssignMenus] = useState(false); const [record, setRecord] = useState(null); const [pagination, setPagination] = useState({ sizeCanChange: true, @@ -40,11 +40,15 @@ function UserManage() { pageSize: pagination.pageSize, }); - const showAssignRolesModal = (payload) => { + const showAssignMenusModal = (payload) => { setRecord(payload); - setVisibleAssignRoles(true); + setVisibleAssignMenus(true); }; + const showAssignResourcesModal = () => { + + } + const showModal = (payload?) => { payload && setRecord(payload); setVisible(true); @@ -65,7 +69,10 @@ function UserManage() { await onDelete(record); break case TableOptions.ASSIGN_MENUS: - showAssignRolesModal(record); + showAssignMenusModal(record); + break + case TableOptions.ASSIGN_RESOURCES: + showAssignResourcesModal(record); break } } @@ -108,14 +115,14 @@ function UserManage() { const handleConfirm = async () => { setRecord(null); setVisible(false); - setVisibleAssignRoles(false); + setVisibleAssignMenus(false); await refetch(); }; const handleCancel = () => { setRecord(null); setVisible(false); - setVisibleAssignRoles(false); + setVisibleAssignMenus(false); }; useEffect(() => { @@ -135,10 +142,10 @@ function UserManage() { visible={visible} record={record} /> - 角色管理 diff --git a/server/prisma/migrations/20250311054029_/migration.sql b/server/prisma/migrations/20250311054029_/migration.sql deleted file mode 100644 index 7b74993..0000000 --- a/server/prisma/migrations/20250311054029_/migration.sql +++ /dev/null @@ -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"); diff --git a/server/prisma/migrations/20250310150829_/migration.sql b/server/prisma/migrations/20250312021820_/migration.sql similarity index 97% rename from server/prisma/migrations/20250310150829_/migration.sql rename to server/prisma/migrations/20250312021820_/migration.sql index a2085c8..bc1904c 100644 --- a/server/prisma/migrations/20250310150829_/migration.sql +++ b/server/prisma/migrations/20250312021820_/migration.sql @@ -80,6 +80,7 @@ CREATE TABLE "sys_resource" ( "updated_at" TIMESTAMP(3) NOT NULL, "name" TEXT NOT NULL, "path" TEXT, + "permission_key" TEXT, "parent_id" TEXT, "sort" INTEGER NOT NULL DEFAULT 0, "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"); -- 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 -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 CREATE INDEX "sys_resource_permission_resource_id_idx" ON "sys_resource_permission"("resource_id"); diff --git a/server/prisma/schema.prisma b/server/prisma/schema.prisma index 62bff8a..8a47729 100644 --- a/server/prisma/schema.prisma +++ b/server/prisma/schema.prisma @@ -109,7 +109,7 @@ model SysResource { updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间 name String /// 菜单名称或者接口名称 path String? @unique() /// 菜单或api路径 - permission String? @unique() /// 权限字符 + permissionKey String? @unique() @map("permission_key") /// 权限字符 parentId String? @map("parent_id") sort Int @default(0) type Int /// 资源类型 0:菜单 1:api diff --git a/server/prisma/seed.ts b/server/prisma/seed.ts index 6aa0fe2..7c120c1 100644 --- a/server/prisma/seed.ts +++ b/server/prisma/seed.ts @@ -7,27 +7,30 @@ enum PermissionType { } 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 = [ { name: '用户管理', path: 'user', type: PermissionType.Menu }, { name: '角色管理', path: 'role', type: PermissionType.Menu }, { name: '菜单管理', path: 'menu', 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({ data: menu, }); @@ -39,8 +42,24 @@ async function main() { await _prisma.sysResourcePermission.create({ 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() diff --git a/server/src/common/guard/permission.guard.ts b/server/src/common/guard/permission.guard.ts index 8339240..8101127 100644 --- a/server/src/common/guard/permission.guard.ts +++ b/server/src/common/guard/permission.guard.ts @@ -52,25 +52,24 @@ export class PermissionGuard implements CanActivate { // 所有用户的code const roleCodes = userRoles.map((item) => item.code); const roleIds = userRoles.map((item) => item.id); + // 如果是管理员 直接通过验证不需要进行接口鉴权 if (roleCodes.includes(Role.Admin)) { request['isAdmin'] = true; return true; } - console.log(roleIds); - - // 已知 权限字符 通过权限字符找出权限id 再根据权限id 找出拥有权限的角色code - const hasPermission = await this.prisma.sysRolePermission.findMany({ - where: { - roleId: { - in: roleIds, - }, - }, - }); - // TODO - - console.log(hasPermission); + // // 已知 权限字符 通过权限字符找出权限id 再根据权限id 找出拥有权限的角色code + // const hasPermission = await this.prisma.sysRolePermission.findMany({ + // where: { + // roleId: { + // in: roleIds, + // }, + // }, + // }); + // // TODO + // + // console.log(hasPermission); // 判断当前接口是否存在权限 throw new ForbiddenException('无权访问!'); diff --git a/server/src/modules/system/menu/menu.service.ts b/server/src/modules/system/menu/menu.service.ts index 0f15b72..63b5e19 100644 --- a/server/src/modules/system/menu/menu.service.ts +++ b/server/src/modules/system/menu/menu.service.ts @@ -67,11 +67,9 @@ export class MenuService { async update(id: string, data: UpdateMenuDto) { const doesExists = await this.prisma.sysResource.findUnique({ - where: { path: data.path }, + where: { path: data.path, NOT: { id } }, }); - console.log(doesExists); - if (doesExists) { throw new HttpException('菜单路径不可重复!', HttpStatus.NOT_FOUND); } diff --git a/server/src/modules/system/permission/dto/grant-menu.dto.ts b/server/src/modules/system/permission/dto/grant-menu.dto.ts index 68d4d57..be20d3b 100644 --- a/server/src/modules/system/permission/dto/grant-menu.dto.ts +++ b/server/src/modules/system/permission/dto/grant-menu.dto.ts @@ -2,5 +2,5 @@ import { IsString } from 'class-validator'; export class GrantMenuDto { @IsString({ each: true }) - menuIds: string[]; + ids: string[]; } diff --git a/server/src/modules/system/permission/permission.controller.ts b/server/src/modules/system/permission/permission.controller.ts index 38d7281..4ab24fe 100644 --- a/server/src/modules/system/permission/permission.controller.ts +++ b/server/src/modules/system/permission/permission.controller.ts @@ -2,18 +2,37 @@ import { Controller, Body, Put, Param, Get } from '@nestjs/common'; import { PermissionService } from './permission.service'; import { GrantMenuDto } from './dto/grant-menu.dto'; +import { PermissionType } from '@/common/enum'; @Controller() export class PermissionController { constructor(private readonly permissionService: PermissionService) {} - @Get('/roles/:roleId/resources') + @Get('/roles/:roleId/menus') 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') - assignMenus(@Param('roleId') roleId: string, @Body() dto: GrantMenuDto) { - return this.permissionService.assignMenus(roleId, dto); + assignResources(@Param('roleId') roleId: string, @Body() data: GrantMenuDto) { + return this.permissionService.assignResources( + roleId, + PermissionType.Menu, + data, + ); } } diff --git a/server/src/modules/system/permission/permission.service.ts b/server/src/modules/system/permission/permission.service.ts index 1a29c6e..96edb66 100644 --- a/server/src/modules/system/permission/permission.service.ts +++ b/server/src/modules/system/permission/permission.service.ts @@ -1,13 +1,14 @@ import { Injectable } from '@nestjs/common'; import { GrantMenuDto } from '@/modules/system/permission/dto/grant-menu.dto'; import { PrismaService } from '@/prisma/prisma.service'; +import { PermissionType } from '@/common/enum'; @Injectable() export class PermissionService { constructor(private readonly prisma: PrismaService) {} - async getRoleMenus(roleId: string) { - const menuIds = await this.prisma.sysResourcePermission.findMany({ + async getRoleResources(roleId: string, type: PermissionType) { + const ids = await this.prisma.sysResourcePermission.findMany({ select: { 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 prisma.sysRolePermission.deleteMany({ - where: { roleId }, + where: { + roleId, + }, }); - // 通过菜单id 找出所有权限id + // 通过资源id 找出所有权限id const rolePermIds = await prisma.sysResourcePermission.findMany({ select: { permissionId: true }, - where: { resourceId: { in: menuIds } }, + where: { resourceId: { in: ids } }, }); // 根据权限id 关联菜单和权限 if (rolePermIds.length > 0) { diff --git a/server/src/modules/system/resource/dto/create-resource.dto.ts b/server/src/modules/system/resource/dto/create-resource.dto.ts index 7c4508d..d0719d1 100644 --- a/server/src/modules/system/resource/dto/create-resource.dto.ts +++ b/server/src/modules/system/resource/dto/create-resource.dto.ts @@ -8,7 +8,7 @@ export class CreateResourceDto { @IsNotEmpty() @IsString() - permission: string; + permissionKey: string; @IsOptional() @IsString() diff --git a/server/src/modules/system/resource/resource.service.ts b/server/src/modules/system/resource/resource.service.ts index 337b9a5..c7f4872 100644 --- a/server/src/modules/system/resource/resource.service.ts +++ b/server/src/modules/system/resource/resource.service.ts @@ -11,7 +11,7 @@ export class ResourceService { async create(data: CreateResourceDto) { const doesExists = await this.prisma.sysResource.findUnique({ - where: { path: data.permission }, + where: { permissionKey: data.permissionKey }, }); if (doesExists) @@ -66,7 +66,7 @@ export class ResourceService { async update(id: string, data: UpdateResourceDto) { const doesExists = await this.prisma.sysResource.findUnique({ - where: { path: data.permission, NOT: { id } }, + where: { permissionKey: data.permissionKey, NOT: { id } }, }); if (doesExists) diff --git a/server/src/modules/system/user/user.controller.ts b/server/src/modules/system/user/user.controller.ts index 5056e58..b475798 100644 --- a/server/src/modules/system/user/user.controller.ts +++ b/server/src/modules/system/user/user.controller.ts @@ -33,7 +33,6 @@ export class UserController { getUserInfo(@Request() req) { const { sub } = req['user']; const { isAdmin } = req; - console.log(isAdmin); return this.userService.getUserInfo(sub, isAdmin); }