Răsfoiți Sursa

✨ feat(permission): 实现基于角色的权限控制系统

- 添加权限检查Hook usePermission,支持多种权限验证方式
- 实现菜单权限过滤功能,根据用户权限动态显示菜单项
- 创建权限API端点,用于获取用户权限列表
- 开发PermissionService服务,处理权限查询和验证逻辑
- 完善角色-权限关联关系,支持权限继承和权限过滤

✨ feat(menu): 实现权限驱动的菜单过滤

- 添加菜单权限过滤逻辑,基于permission配置过滤菜单项
- 实现递归菜单权限检查,支持多层级子菜单过滤
- 优化无权限菜单的处理逻辑,自动隐藏无权限的菜单组
- 改进菜单渲染逻辑,确保只显示有权限访问的菜单项
yourname 7 luni în urmă
părinte
comite
85e2c40923

+ 108 - 0
src/client/admin/hooks/usePermission.ts

@@ -0,0 +1,108 @@
+import { useCallback, useMemo } from 'react';
+import { useQuery } from '@tanstack/react-query';
+import { useAuth } from './AuthProvider';
+import { authClient } from '@/client/api';
+
+/**
+ * 权限检查Hook
+ * 用于检查当前用户是否拥有特定权限
+ *
+ * 完整的权限检查逻辑:
+ * 1. 管理员角色拥有所有权限
+ * 2. 基于角色权限关联表进行权限验证
+ * 3. 支持权限编码和权限名称两种方式的权限检查
+ */
+export const usePermission = () => {
+  const { user } = useAuth();
+
+  // 获取用户权限列表
+  const { data: permissionData, isLoading, error } = useQuery({
+    queryKey: ['userPermissions', user?.id],
+    queryFn: async () => {
+      if (!user?.id) return [];
+      
+      try {
+        const response = await authClient.permissions.$get();
+        if (response.status !== 200) {
+          console.error('获取权限失败:', response.status);
+          return [];
+        }
+        
+        const data = await response.json();
+        return data.permissions || [];
+      } catch (error) {
+        console.error('获取用户权限失败:', error);
+        return [];
+      }
+    },
+    enabled: !!user?.id && !!user.roles, // 只在用户已登录且有角色时获取权限
+    staleTime: 5 * 60 * 1000, // 5分钟内缓存有效
+    retry: 1
+  });
+
+  const userPermissions = useMemo(() => permissionData || [], [permissionData]);
+
+  /**
+   * 检查用户是否拥有指定权限编码
+   * @param permissionCode 权限编码(如 'user:manage')
+   * @returns boolean 是否有权限
+   */
+  const hasPermission = useCallback((permissionCode: string) => {
+    if (!user?.roles) return false;
+    
+    // 管理员角色拥有所有权限
+    const hasAdminRole = user.roles.some(role => role.name === 'admin');
+    if (hasAdminRole) return true;
+    
+    // 基于权限列表进行权限检查
+    return userPermissions.includes(permissionCode);
+  }, [user, userPermissions]);
+
+  /**
+   * 检查用户是否拥有多个权限中的任意一个
+   * @param permissions 权限标识符数组
+   * @returns boolean 是否有任意一个权限
+   */
+  const hasAnyPermission = useCallback((permissions: string[]) => {
+    return permissions.some(permission => hasPermission(permission));
+  }, [hasPermission]);
+
+  /**
+   * 检查用户是否拥有所有指定权限
+   * @param permissions 权限标识符数组
+   * @returns boolean 是否拥有所有权限
+   */
+  const hasAllPermissions = useCallback((permissions: string[]) => {
+    return permissions.every(permission => hasPermission(permission));
+  }, [hasPermission]);
+
+  /**
+   * 获取用户角色列表
+   * @returns 角色名称数组
+   */
+  const getUserRoles = useCallback(() => {
+    return user?.roles?.map(role => role.name) || [];
+  }, [user]);
+
+  /**
+   * 检查用户是否拥有特定角色
+   * @param roleName 角色名称
+   * @returns boolean 是否拥有该角色
+   */
+  const hasRole = useCallback((roleName: string) => {
+    if (!user?.roles) return false;
+    return user.roles.some(role => role.name === roleName);
+  }, [user]);
+
+  return {
+    hasPermission,
+    hasAnyPermission,
+    hasAllPermissions,
+    hasRole,
+    getUserRoles,
+    roles: user?.roles || [],
+    permissions: userPermissions,
+    isLoadingPermissions: isLoading,
+    permissionsError: error
+  };
+};

+ 41 - 5
src/client/admin/layouts/MainLayout.tsx

@@ -15,6 +15,7 @@ import {
 } from '@ant-design/icons';
 import { useAuth } from '../hooks/AuthProvider';
 import { useMenu, useMenuSearch, type MenuItem } from '../menu';
+import { usePermission } from '../hooks/usePermission';
 import { getGlobalConfig } from '@/client/utils/utils';
 
 const { Header, Sider, Content } = Layout;
@@ -28,9 +29,10 @@ export const MainLayout = () => {
   const [showBackTop, setShowBackTop] = useState(false);
   const location = useLocation();
   
-  // 使用菜单hook
+  // 使用权限和菜单hook
+  const { hasPermission } = usePermission();
   const {
-    menuItems,
+    menuItems: allMenuItems,
     userMenuItems,
     openKeys,
     collapsed,
@@ -60,12 +62,46 @@ export const MainLayout = () => {
     return null;
   };
   
+  // 权限过滤后的菜单项
+  const filteredByPermissionMenuItems = useMemo(() => {
+    const filterMenuByPermission = (items: MenuItem[]): MenuItem[] => {
+      return items
+        .filter(item => {
+          // 如果没有配置权限,直接显示
+          if (!item.permission) return true;
+          
+          // 检查用户是否有该权限
+          return hasPermission(item.permission);
+        })
+        .map(item => {
+          // 递归处理子菜单
+          if (item.children) {
+            const filteredChildren = filterMenuByPermission(item.children);
+            return {
+              ...item,
+              children: filteredChildren.length > 0 ? filteredChildren : undefined
+            };
+          }
+          return item;
+        })
+        .filter(item => {
+          // 如果子菜单被过滤为空,且没有路径,则不显示该菜单项
+          if (item.children && item.children.length === 0 && !item.path) {
+            return false;
+          }
+          return true;
+        });
+    };
+
+    return filterMenuByPermission(allMenuItems);
+  }, [allMenuItems, hasPermission]);
+
   // 使用菜单搜索hook
   const {
     searchText,
     setSearchText,
     filteredMenuItems
-  } = useMenuSearch(menuItems);
+  } = useMenuSearch(filteredByPermissionMenuItems);
   
   // 获取当前选中的菜单项
   const selectedKey = useMemo(() => {
@@ -81,8 +117,8 @@ export const MainLayout = () => {
       return null;
     };
     
-    return findSelectedKey(menuItems) || '';
-  }, [location.pathname, menuItems]);
+    return findSelectedKey(filteredByPermissionMenuItems) || '';
+  }, [location.pathname, filteredByPermissionMenuItems]);
   
   // 检测滚动位置,控制回到顶部按钮显示
   useEffect(() => {

+ 3 - 1
src/server/api/auth/index.ts

@@ -4,12 +4,14 @@ import logoutRoute from './logout';
 import meRoute from './me/get';
 import registerRoute from './register/create';
 import ssoVerify from './sso-verify';
+import permissionsRoute from './permissions/get';
 
 const app = new OpenAPIHono()
   .route('/', loginRoute)
   .route('/', logoutRoute)
   .route('/', meRoute)
   .route('/', registerRoute)
-  .route('/', ssoVerify);
+  .route('/', ssoVerify)
+  .route('/', permissionsRoute);
 
 export default app;

+ 64 - 0
src/server/api/auth/permissions/get.ts

@@ -0,0 +1,64 @@
+import { createRoute, OpenAPIHono } from '@hono/zod-openapi'
+import { ErrorSchema } from '@/server/utils/errorHandler'
+import { authMiddleware } from '@/server/middleware/auth.middleware'
+import { AuthContext } from '@/server/types/context'
+import { PermissionService } from '@/server/modules/permissions/permission.service'
+import { AppDataSource } from '@/server/data-source'
+import { z } from '@hono/zod-openapi'
+
+const permissionsService = new PermissionService(AppDataSource)
+
+const PermissionsResponseSchema = z.object({
+  permissions: z.array(z.string()).openapi({
+    description: '用户权限编码列表',
+    example: ['user:manage', 'client:view', 'admin:access']
+  })
+})
+
+const routeDef = createRoute({
+  method: 'get',
+  path: '/permissions',
+  middleware: [authMiddleware],
+  responses: {
+    200: {
+      description: '获取用户权限列表成功',
+      content: {
+        'application/json': {
+          schema: PermissionsResponseSchema
+        }
+      }
+    },
+    401: {
+      description: '未授权',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    },
+    500: {
+      description: '服务器错误',
+      content: {
+        'application/json': {
+          schema: ErrorSchema
+        }
+      }
+    }
+  }
+})
+
+const app = new OpenAPIHono<AuthContext>().openapi(routeDef, async (c) => {
+  try {
+    const user = c.get('user')
+    const permissions = await permissionsService.getUserPermissions(user.id)
+    
+    return c.json({
+      permissions
+    }, 200)
+  } catch (error) {
+    console.error('获取用户权限失败:', error)
+    return c.json({ code: 500, message: '获取用户权限失败' }, 500)
+  }
+})
+
+export default app

+ 113 - 0
src/server/modules/permissions/permission.service.ts

@@ -0,0 +1,113 @@
+import { DataSource, Repository } from 'typeorm';
+import { Permission } from './permission.entity';
+import { RolePermission } from './permission.entity';
+import { UserEntity as User } from '@/server/modules/users/user.entity';
+import { Role } from '@/server/modules/users/role.entity';
+
+export class PermissionService {
+  private permissionRepository: Repository<Permission>;
+  private rolePermissionRepository: Repository<RolePermission>;
+  private roleRepository: Repository<Role>;
+  private userRepository: Repository<User>;
+
+  constructor(private dataSource: DataSource) {
+    this.permissionRepository = this.dataSource.getRepository(Permission);
+    this.rolePermissionRepository = this.dataSource.getRepository(RolePermission);
+    this.roleRepository = this.dataSource.getRepository(Role);
+    this.userRepository = this.dataSource.getRepository(User);
+  }
+
+  /**
+   * 获取用户的权限编码列表
+   */
+  async getUserPermissions(userId: number): Promise<string[]> {
+    try {
+      // 检查用户是否有管理员角色
+      const user = await this.userRepository.findOne({
+        where: { id: userId },
+        relations: ['roles']
+      });
+
+      if (!user || !user.roles) {
+        return [];
+      }
+
+      // 管理员角色拥有所有权限
+      if (user.roles.some(role => role.name === 'admin')) {
+        const allPermissions = await this.permissionRepository.find({
+          where: { isActive: 1 }
+        });
+        return allPermissions.map(p => p.code);
+      }
+
+      // 获取用户的权限
+      const roleIds = user.roles.map(role => role.id);
+      
+      if (roleIds.length === 0) {
+        return [];
+      }
+
+      // 查询角色的权限编码
+      const permissions = await this.permissionRepository
+        .createQueryBuilder('permission')
+        .innerJoin(
+          RolePermission,
+          'rolePermission',
+          'permission.id = rolePermission.permissionId'
+        )
+        .where('rolePermission.roleId IN (:...roleIds)', { roleIds })
+        .andWhere('permission.isActive = 1')
+        .getMany();
+
+      return permissions.filter(p => p).map(p => p.code);
+    } catch (error) {
+      console.error('Error getting user permissions:', error);
+      throw new Error('Failed to get user permissions');
+    }
+  }
+
+  /**
+   * 检查用户是否拥有指定权限
+   */
+  async hasPermission(userId: number, permissionCode: string): Promise<boolean> {
+    try {
+      const permissions = await this.getUserPermissions(userId);
+      return permissions.includes(permissionCode);
+    } catch (error) {
+      console.error('Error checking user permission:', error);
+      return false;
+    }
+  }
+
+  /**
+   * 获取用户的角色列表
+   */
+  async getUserRoles(userId: number): Promise<string[]> {
+    try {
+      const user = await this.userRepository.findOne({
+        where: { id: userId },
+        relations: ['roles']
+      });
+      return user?.roles?.map(role => role.name) || [];
+    } catch (error) {
+      console.error('Error getting user roles:', error);
+      return [];
+    }
+  }
+
+  /**
+   * 获取用户的角色ID列表
+   */
+  async getUserRoleIds(userId: number): Promise<number[]> {
+    try {
+      const user = await this.userRepository.findOne({
+        where: { id: userId },
+        relations: ['roles']
+      });
+      return user?.roles?.map(role => role.id) || [];
+    } catch (error) {
+      console.error('Error getting user role IDs:', error);
+      return [];
+    }
+  }
+}

+ 6 - 2
src/server/modules/users/role.entity.ts

@@ -1,7 +1,8 @@
-import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn } from 'typeorm';
+import { Entity, PrimaryGeneratedColumn, Column, CreateDateColumn, UpdateDateColumn, OneToMany } from 'typeorm';
 import { z } from '@hono/zod-openapi';
+import { RolePermission } from '@/server/modules/permissions/permission.entity';
+import { Permission } from '@/server/modules/permissions/permission.entity';
 
-export type Permission = string;
 
 export const RoleSchema = z.object({
   id: z.number().int().positive().openapi({
@@ -40,6 +41,9 @@ export class Role {
   @UpdateDateColumn({ name: 'updated_at', type: 'timestamp' })
   updatedAt!: Date;
 
+  @OneToMany(() => RolePermission, rolePermission => rolePermission.role)
+  rolePermissions!: RolePermission[];
+
   constructor(partial?: Partial<Role>) {
     Object.assign(this, partial);
   }