feat: RBAC权限管理

This commit is contained in:
2025-03-10 23:55:10 +08:00
parent cbd06dcf54
commit e7bd916cae
13 changed files with 142 additions and 110 deletions

View File

@@ -27,8 +27,8 @@ export function getColumns(callback: (type: TableOptions, record: any) => Promis
align: 'center', align: 'center',
}, },
{ {
title: '菜单url', title: '菜单path',
dataIndex: 'url', dataIndex: 'path',
align: 'center', align: 'center',
}, },
{ {

View File

@@ -88,8 +88,8 @@ 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="菜单url" field="url" rules={[{required: true}]}> <Form.Item label="菜单path" field="path" rules={[{required: true}]}>
<Input placeholder="请输入菜单url"/> <Input placeholder="请输入菜单path"/>
</Form.Item> </Form.Item>
<Form.Item label="排序" field="sort"> <Form.Item label="排序" field="sort">
<InputNumber placeholder="请输入排序"/> <InputNumber placeholder="请输入排序"/>

View File

@@ -20,7 +20,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.map(item=>Number(item))); await grantPermission(values.id, checkedKeys);
Message.success(`操作成功!`); Message.success(`操作成功!`);
handleConfirm(); handleConfirm();
} catch (e) { } catch (e) {
@@ -41,13 +41,12 @@ function GrantPermission(props: Props) {
const [handleFinished, setHandleFinished] = useState(false); const [handleFinished, setHandleFinished] = useState(false);
const handleMenuAndKeys = (menu, menuIds) => { const handleMenuAndKeys = (menu, menuIds) => {
let newMenuIds = menuIds.map((menuId) => menuId.toString()); let newMenuIds = menuIds.map((menuId) => menuId);
function handleMenu(_menu) { function handleMenu(_menu) {
_menu.forEach((item) => { _menu.forEach((item) => {
// 组件需要的key必须为string // 组件需要的key必须为string
item.id = item.id.toString(); item.parentId = item.id;
item.parentId = item.id.toString();
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))) {

View File

@@ -15,7 +15,7 @@ export type IRoute = AuthParams & {
const generatorMenu = (data, parentId = '-') => { const generatorMenu = (data, parentId = '-') => {
const menu = []; const menu = [];
for (const item of data) { for (const item of data) {
item.key = item.url; item.key = item.path;
if ((item.parentId || '-') === parentId) { if ((item.parentId || '-') === parentId) {
const children = generatorMenu(data, item.id); const children = generatorMenu(data, item.id);
if (children.length) { if (children.length) {

View File

@@ -2,3 +2,10 @@
# 种子文件 # 种子文件
npx prisma db seed npx prisma db seed
``` ```
```json
{
"prisma": {
"seed": "ts-node prisma/seed.ts"
}
}
```

View File

@@ -57,35 +57,11 @@ CREATE TABLE "sys_permission" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP, "created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL, "updated_at" TIMESTAMP(3) NOT NULL,
"type" INTEGER NOT NULL, "name" TEXT,
CONSTRAINT "sys_permission_pkey" PRIMARY KEY ("id") CONSTRAINT "sys_permission_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "sys_menu" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"url" TEXT NOT NULL,
"parent_id" TEXT,
"sort" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "sys_menu_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sys_menu_permission" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"menu_id" TEXT NOT NULL,
"permission_id" TEXT NOT NULL,
CONSTRAINT "sys_menu_permission_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "sys_role_permission" ( CREATE TABLE "sys_role_permission" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
@@ -97,6 +73,31 @@ CREATE TABLE "sys_role_permission" (
CONSTRAINT "sys_role_permission_pkey" PRIMARY KEY ("id") CONSTRAINT "sys_role_permission_pkey" PRIMARY KEY ("id")
); );
-- CreateTable
CREATE TABLE "sys_resource" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"name" TEXT NOT NULL,
"path" TEXT,
"parent_id" TEXT,
"sort" INTEGER NOT NULL DEFAULT 0,
"type" INTEGER NOT NULL,
CONSTRAINT "sys_resource_pkey" PRIMARY KEY ("id")
);
-- CreateTable
CREATE TABLE "sys_resource_permission" (
"id" TEXT NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL,
"resource_id" TEXT NOT NULL,
"permission_id" TEXT NOT NULL,
CONSTRAINT "sys_resource_permission_pkey" PRIMARY KEY ("id")
);
-- CreateTable -- CreateTable
CREATE TABLE "sys_config" ( CREATE TABLE "sys_config" (
"id" TEXT NOT NULL, "id" TEXT NOT NULL,
@@ -125,10 +126,7 @@ CREATE INDEX "sys_user_role_user_id_idx" ON "sys_user_role"("user_id");
CREATE INDEX "sys_user_role_role_id_idx" ON "sys_user_role"("role_id"); CREATE INDEX "sys_user_role_role_id_idx" ON "sys_user_role"("role_id");
-- CreateIndex -- CreateIndex
CREATE INDEX "sys_menu_permission_menu_id_idx" ON "sys_menu_permission"("menu_id"); CREATE UNIQUE INDEX "sys_permission_name_key" ON "sys_permission"("name");
-- CreateIndex
CREATE INDEX "sys_menu_permission_permission_id_idx" ON "sys_menu_permission"("permission_id");
-- CreateIndex -- CreateIndex
CREATE INDEX "sys_role_permission_role_id_idx" ON "sys_role_permission"("role_id"); CREATE INDEX "sys_role_permission_role_id_idx" ON "sys_role_permission"("role_id");
@@ -136,5 +134,17 @@ CREATE INDEX "sys_role_permission_role_id_idx" ON "sys_role_permission"("role_id
-- CreateIndex -- CreateIndex
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
CREATE UNIQUE INDEX "sys_resource_name_type_key" ON "sys_resource"("name", "type");
-- CreateIndex
CREATE UNIQUE INDEX "sys_resource_path_key" ON "sys_resource"("path");
-- CreateIndex
CREATE INDEX "sys_resource_permission_resource_id_idx" ON "sys_resource_permission"("resource_id");
-- CreateIndex
CREATE INDEX "sys_resource_permission_permission_id_idx" ON "sys_resource_permission"("permission_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "sys_config_key_key" ON "sys_config"("key"); CREATE UNIQUE INDEX "sys_config_key_key" ON "sys_config"("key");

View File

@@ -77,45 +77,16 @@ model File {
// 权限表 // 权限表
model SysPermission { model SysPermission {
id String @id @default(cuid()) /// 主键id id String @id @default(cuid()) /// 主键id
createdAt DateTime @default(now()) @map("created_at") /// 创建时间 createdAt DateTime @default(now()) @map("created_at") /// 创建时间
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间 updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
type Int /// 权限类型 name String? @unique // 权限字符
sysRolePermission SysRolePermission[] sysRolePermission SysRolePermission[]
sysMenuPermission SysMenuPermission[] sysResourcePermission SysResourcePermission[]
@@map("sys_permission") @@map("sys_permission")
} }
// 系统菜单表
model SysMenu {
id String @id @default(cuid()) /// 主键id
createdAt DateTime @default(now()) @map("created_at") /// 创建时间
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
name String
url String
parentId String? @map("parent_id")
sort Int @default(0)
sysMenuPermission SysMenuPermission[]
@@map("sys_menu")
}
// 菜单权限关联表
model SysMenuPermission {
id String @id @default(cuid()) /// 主键id
createdAt DateTime @default(now()) @map("created_at") /// 创建时间
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
menuId String @map("menu_id")
permissionId String @map("permission_id")
menu SysMenu @relation(fields: [menuId], references: [id], onDelete: Cascade)
permission SysPermission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@index([menuId])
@@index([permissionId])
@@map("sys_menu_permission")
}
// 角色权限关联表 // 角色权限关联表
model SysRolePermission { model SysRolePermission {
id String @id @default(cuid()) /// 主键id id String @id @default(cuid()) /// 主键id
@@ -131,6 +102,38 @@ model SysRolePermission {
@@map("sys_role_permission") @@map("sys_role_permission")
} }
// 系统资源表
model SysResource {
id String @id @default(cuid()) /// 主键id
createdAt DateTime @default(now()) @map("created_at") /// 创建时间
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
name String
path String? /// 菜单或api路径
parentId String? @map("parent_id")
sort Int @default(0)
type Int /// 资源类型 0:菜单 1:api
sysResourcePermission SysResourcePermission[]
@@unique([name, type])
@@unique([path])
@@map("sys_resource")
}
// 系统资源权限表
model SysResourcePermission {
id String @id @default(cuid()) /// 主键id
createdAt DateTime @default(now()) @map("created_at") /// 创建时间
updatedAt DateTime @updatedAt @map("updated_at") /// 修改时间
resourceId String @map("resource_id")
resource SysResource @relation(fields: [resourceId], references: [id], onDelete: Cascade)
permissionId String @map("permission_id")
permission SysPermission @relation(fields: [permissionId], references: [id], onDelete: Cascade)
@@index([resourceId])
@@index([permissionId])
@@map("sys_resource_permission")
}
// 系统配置 // 系统配置
model SysConfig { model SysConfig {
id String @id @default(cuid()) /// 主键id id String @id @default(cuid()) /// 主键id

View File

@@ -3,6 +3,7 @@ const prisma = new PrismaClient();
enum PermissionType { enum PermissionType {
Menu, Menu,
API,
} }
async function main() { async function main() {
@@ -18,23 +19,25 @@ async function main() {
}, },
}); });
const menus = [ const menus = [
{ name: '用户管理', url: 'user' }, { name: '用户管理', path: 'user', type: PermissionType.Menu },
{ name: '角色管理', url: 'role' }, { name: '角色管理', path: 'role', type: PermissionType.Menu },
{ name: '菜单管理', url: 'menu' }, { name: '菜单管理', path: 'menu', type: PermissionType.Menu },
{ name: '系统配置', url: 'config' }, { name: '系统配置', path: 'config', type: PermissionType.Menu },
]; ];
for (const menu of menus) { for (const menu of menus) {
// 创建菜单 // 创建菜单
await prisma.$transaction(async (_prisma) => { await prisma.$transaction(async (_prisma) => {
const { id: menuId } = await _prisma.sysMenu.create({ data: menu }); const { id: resourceId } = await _prisma.sysResource.create({
data: menu,
});
// 创建菜单权限 // 创建菜单权限
const { id: permissionId } = await _prisma.sysPermission.create({ const { id: permissionId } = await _prisma.sysPermission.create({
data: { type: PermissionType.Menu }, data: {},
}); });
// 关联权限和菜单 // 关联权限和菜单
await _prisma.sysMenuPermission.create({ await _prisma.sysResourcePermission.create({
data: { menuId, permissionId }, data: { resourceId, permissionId },
}); });
}); });
} }

View File

@@ -10,4 +10,5 @@ export enum RedisGroup {
export enum PermissionType { export enum PermissionType {
Menu, Menu,
API,
} }

View File

@@ -7,7 +7,7 @@ export class CreateMenuDto {
@IsNotEmpty() @IsNotEmpty()
@IsString() @IsString()
url: string; path: string;
@IsOptional() @IsOptional()
@IsString() @IsString()
@@ -16,4 +16,8 @@ export class CreateMenuDto {
@IsOptional() @IsOptional()
@IsInt() @IsInt()
sort?: number; sort?: number;
@IsOptional()
@IsInt()
type: number;
} }

View File

@@ -11,8 +11,9 @@ export class MenuService {
findOne(params: { id?: string; excludeId?: string }) { findOne(params: { id?: string; excludeId?: string }) {
const { id, excludeId } = params; const { id, excludeId } = params;
return this.prisma.sysMenu.findFirst({ return this.prisma.sysResource.findFirst({
where: { where: {
type: PermissionType.Menu,
...(id ? { id } : {}), ...(id ? { id } : {}),
...(excludeId ? { NOT: { id: excludeId } } : {}), ...(excludeId ? { NOT: { id: excludeId } } : {}),
}, },
@@ -20,18 +21,20 @@ export class MenuService {
} }
findOneById(id: string) { findOneById(id: string) {
return this.prisma.sysMenu.findUnique({ where: { id } }); return this.prisma.sysResource.findUnique({ where: { id } });
} }
async create(data: CreateMenuDto) { async create(data: CreateMenuDto) {
await this.prisma.$transaction(async (prisma) => { await this.prisma.$transaction(async (prisma) => {
const { id: menuId } = await prisma.sysMenu.create({ data }); const { id: resourceId } = await prisma.sysResource.create({ data });
// 创建菜单权限 // 创建菜单权限
const { id: permissionId } = await prisma.sysPermission.create({ const { id: permissionId } = await prisma.sysPermission.create({
data: { type: PermissionType.Menu }, data: {},
});
// // 关联权限和菜单
await prisma.sysResourcePermission.create({
data: { resourceId, permissionId },
}); });
// 关联权限和菜单
await prisma.sysMenuPermission.create({ data: { menuId, permissionId } });
}); });
} }
@@ -50,23 +53,23 @@ export class MenuService {
} }
async getTreeMenu() { async getTreeMenu() {
const query: Prisma.SysMenuFindManyArgs = { const query: Prisma.SysResourceFindManyArgs = {
orderBy: [{ sort: 'desc' }, { createdAt: 'desc' }], orderBy: [{ sort: 'desc' }, { createdAt: 'desc' }],
}; };
const list = await this.prisma.sysMenu.findMany(query); const list = await this.prisma.sysResource.findMany(query);
return this.generatorMenu(list); return this.generatorMenu(list);
} }
findAll() { findAll() {
const query: Prisma.SysMenuFindManyArgs = { const query: Prisma.SysResourceFindManyArgs = {
select: { select: {
id: true, id: true,
name: true, name: true,
url: true, path: true,
parentId: true, parentId: true,
}, },
}; };
return this.prisma.sysMenu.findMany(query); return this.prisma.sysResource.findMany(query);
} }
async update(id: string, data: UpdateMenuDto) { async update(id: string, data: UpdateMenuDto) {
@@ -82,18 +85,18 @@ export class MenuService {
); );
} }
await this.prisma.sysMenu.update({ data, where: { id } }); await this.prisma.sysResource.update({ data, where: { id } });
} }
async remove(menuId: string) { async remove(resourceId: string) {
const doesExists = await this.findOneById(menuId); const doesExists = await this.findOneById(resourceId);
if (!doesExists) { if (!doesExists) {
throw new HttpException('菜单不存在!', HttpStatus.NOT_FOUND); throw new HttpException('菜单不存在!', HttpStatus.NOT_FOUND);
} }
// 判断是否存在子菜单 // 判断是否存在子菜单
const doesExistsChildren = await this.prisma.sysMenu.findFirst({ const doesExistsChildren = await this.prisma.sysResource.findFirst({
where: { parentId: menuId }, where: { parentId: resourceId },
}); });
if (doesExistsChildren) { if (doesExistsChildren) {
@@ -108,14 +111,14 @@ export class MenuService {
// 因为schema文件中设置了onDelete: Cascade 删除菜单会删除关联表数据 则无法找出对应权限 // 因为schema文件中设置了onDelete: Cascade 删除菜单会删除关联表数据 则无法找出对应权限
await prisma.sysPermission.deleteMany({ await prisma.sysPermission.deleteMany({
where: { where: {
sysMenuPermission: { sysResourcePermission: {
some: { menuId }, some: { resourceId },
}, },
}, },
}); });
//step2 删除菜单数据 //step2 删除菜单数据
await prisma.sysMenu.delete({ where: { id: menuId } }); await prisma.sysResourcePermission.delete({ where: { id: resourceId } });
}); });
} }
} }

View File

@@ -7,9 +7,9 @@ export class PermissionService {
constructor(private readonly prisma: PrismaService) {} constructor(private readonly prisma: PrismaService) {}
async getMenu(id: string) { async getMenu(id: string) {
const menuIds = await this.prisma.sysMenuPermission.findMany({ const menuIds = await this.prisma.sysResourcePermission.findMany({
select: { select: {
menuId: true, resourceId: true,
}, },
where: { where: {
permission: { permission: {
@@ -20,7 +20,7 @@ export class PermissionService {
}, },
}); });
return menuIds.map((item) => item.menuId); return menuIds.map((item) => item.resourceId);
} }
async grantMenu(id: string, { menuIds }: GrantMenuDto) { async grantMenu(id: string, { menuIds }: GrantMenuDto) {
@@ -30,9 +30,9 @@ export class PermissionService {
where: { roleId: id }, where: { roleId: id },
}); });
// 通过菜单id 找出所有权限id // 通过菜单id 找出所有权限id
const rolePermIds = await prisma.sysMenuPermission.findMany({ const rolePermIds = await prisma.sysResourcePermission.findMany({
select: { permissionId: true }, select: { permissionId: true },
where: { menuId: { in: menuIds } }, where: { resourceId: { in: menuIds } },
}); });
// 根据权限id 关联菜单和权限 // 根据权限id 关联菜单和权限
if (rolePermIds.length > 0) { if (rolePermIds.length > 0) {

View File

@@ -13,7 +13,7 @@ import { QueryListDto } from './dto/query-list.dto';
import * as bcrypt from 'bcryptjs'; import * as bcrypt from 'bcryptjs';
import { UpdatePasswordDto } from './dto/update-password.dto'; import { UpdatePasswordDto } from './dto/update-password.dto';
import { SkipAuth } from '@/common/decorators/skip-auth.decorator'; import { SkipAuth } from '@/common/decorators/skip-auth.decorator';
import { RedisGroup } from '@/common/enum'; import { PermissionType, RedisGroup } from '@/common/enum';
import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager'; import { Cache, CACHE_MANAGER } from '@nestjs/cache-manager';
@Injectable() @Injectable()
@@ -126,11 +126,12 @@ export class UserService {
}, },
}); });
const query: Prisma.SysMenuFindManyArgs = { where: {} }; const query: Prisma.SysResourceFindManyArgs = { where: {} };
if (!isAdmin) { if (!isAdmin) {
query.where = { query.where = {
sysMenuPermission: { type: PermissionType.Menu,
sysResourcePermission: {
some: { some: {
permission: { permission: {
sysRolePermission: { sysRolePermission: {
@@ -142,7 +143,8 @@ export class UserService {
}; };
} }
const menus = await this.prisma.sysMenu.findMany(query); const menus = await this.prisma.sysResource.findMany();
return { return {
...user, ...user,
menus, menus,