Просмотр исходного кода

♻️ refactor(users): 分离用户DTO与实体定义

- 创建user.dto.ts文件,将UserSchema、CreateUserDto和UpdateUserDto从user.entity.ts迁移至此
- 更新users/index.ts和files/file.entity.ts中的导入路径,使用新的DTO文件

🔧 fix(permission): 重构权限检查逻辑

- 修改checkPermission函数参数名,从requiredRoles改为requiredPermissions
- 实现基于权限编码的检查机制,替代原有的角色名称检查
- 收集用户所有权限编码并使用Set存储以提高查询效率
- 检查用户是否拥有任一所需权限而非角色
- 更新示例用法中的权限检查参数

✅ test(permission): 添加权限中间件测试脚本

- 创建test-permission-fix.ts测试文件
- 模拟用户、角色和权限数据进行测试
- 验证权限检查功能的正确性,包括有权限和无权限两种场景
- 提供清晰的测试结果输出
yourname 7 месяцев назад
Родитель
Сommit
87df3e75b2

+ 2 - 1
src/server/api/users/index.ts

@@ -1,6 +1,7 @@
 import { createCrudRoutes } from '@/server/utils/generic-crud.routes';
-import { UserEntity, UserSchema, CreateUserDto, UpdateUserDto } from '@/server/modules/users/user.entity';
+import { UserEntity } from '@/server/modules/users/user.entity';
 import { authMiddleware } from '@/server/middleware/auth.middleware';
+import { UserSchema, CreateUserDto, UpdateUserDto } from '@/server/modules/users/user.dto';
 
 // 使用权限配置的示例
 const userRoutes = createCrudRoutes({

+ 20 - 4
src/server/middleware/permission.middleware.ts

@@ -1,12 +1,28 @@
 import { Context, Next } from 'hono';
 import { UserEntity as User } from '../modules/users/user.entity';
+import { RolePermission } from '@/server/modules/permissions/permission.entity';
 
 type PermissionCheck = (user: User) => boolean | Promise<boolean>;
 
-export function checkPermission(requiredRoles: string[]): PermissionCheck {
+export function checkPermission(requiredPermissions: string[]): PermissionCheck {
   return (user: User) => {
     if (!user.roles) return false;
-    return user.roles.some(role => requiredRoles.includes(role.name));
+    
+    // 收集用户所有权限编码
+    const userPermissions = new Set<string>();
+    
+    user.roles.forEach(role => {
+      if (role.rolePermissions) {
+        role.rolePermissions.forEach((rolePermission: RolePermission) => {
+          if (rolePermission.permission) {
+            userPermissions.add(rolePermission.permission.code);
+          }
+        });
+      }
+    });
+    
+    // 检查用户是否拥有任一所需权限
+    return requiredPermissions.some(permission => userPermissions.has(permission));
   };
 }
 
@@ -32,8 +48,8 @@ export function permissionMiddleware(check: PermissionCheck) {
 }
 
 // 示例用法:
-// app.get('/admin', 
+// app.get('/admin',
 //   authMiddleware,
-//   permissionMiddleware(checkPermission(['admin'])),
+//   permissionMiddleware(checkPermission(['system:user:create'])),
 //   (c) => {...}
 // )

+ 2 - 1
src/server/modules/files/file.entity.ts

@@ -1,6 +1,7 @@
 import { Entity, PrimaryGeneratedColumn, Column, Index, ManyToOne, JoinColumn } from 'typeorm';
 import { z } from '@hono/zod-openapi';
-import { UserEntity, UserSchema } from '@/server/modules/users/user.entity';
+import { UserEntity } from '@/server/modules/users/user.entity';
+import { UserSchema } from '@/server/modules/users/user.dto';
 
 @Entity('file')
 export class File {

+ 130 - 0
src/server/modules/users/user.dto.ts

@@ -0,0 +1,130 @@
+import { Role, RoleSchema } from './role.entity';
+import { z } from '@hono/zod-openapi';
+import { DeleteStatus, DisabledStatus } from '@/share/types';
+import { DataScopeType } from '@/server/modules/departments/department.entity';
+
+export const UserSchema = z.object({
+    id: z.number().int().positive().openapi({ description: '用户ID' }),
+    username: z.string().min(3).max(255).openapi({
+      example: 'admin',
+      description: '用户名,3-255个字符'
+    }),
+    password: z.string().min(6).max(255).openapi({
+      example: 'password123',
+      description: '密码,最少6位'
+    }),
+    phone: z.string().max(255).nullable().openapi({
+      example: '13800138000',
+      description: '手机号'
+    }),
+    email: z.string().email().max(255).nullable().openapi({
+      example: 'user@example.com',
+      description: '邮箱'
+    }),
+    nickname: z.string().max(255).nullable().openapi({
+      example: '昵称',
+      description: '用户昵称'
+    }),
+    name: z.string().max(255).nullable().openapi({
+      example: '张三',
+      description: '真实姓名'
+    }),
+    avatar: z.string().max(255).nullable().openapi({
+      example: 'https://example.com/avatar.jpg',
+      description: '用户头像'
+    }),
+    isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
+      example: DisabledStatus.ENABLED,
+      description: '是否禁用(0:启用,1:禁用)'
+    }),
+    isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({
+      example: DeleteStatus.NOT_DELETED,
+      description: '是否删除(0:未删除,1:已删除)'
+    }),
+    roles: z.array(RoleSchema).optional().openapi({
+      example: [
+        { id: 1, name: 'admin',description:'管理员' ,createdAt: new Date(), updatedAt: new Date() }
+      ],
+      description: '用户角色列表'
+    }),
+    defaultDepartmentId: z.number().int().positive().nullable().openapi({ description: '默认部门ID', example: 1 }),
+    dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL }),
+    createdAt: z.date().openapi({ description: '创建时间' }),
+    updatedAt: z.date().openapi({ description: '更新时间' }),
+    createdBy: z.number().int().positive().nullable().openapi({ description: '创建用户ID', example: 1 }),
+    updatedBy: z.number().int().positive().nullable().openapi({ description: '更新用户ID', example: 1 })
+  });
+  
+  export const CreateUserDto = z.object({
+    username: z.string().min(3).max(255).openapi({
+      example: 'admin',
+      description: '用户名,3-255个字符'
+    }),
+    password: z.string().min(6).max(255).openapi({
+      example: 'password123',
+      description: '密码,最少6位'
+    }),
+    phone: z.string().max(255).nullable().openapi({
+      example: '13800138000',
+      description: '手机号'
+    }),
+    email: z.string().email().max(255).nullable().openapi({
+      example: 'user@example.com',
+      description: '邮箱'
+    }),
+    nickname: z.string().max(255).nullable().openapi({
+      example: '昵称',
+      description: '用户昵称'
+    }),
+    name: z.string().max(255).nullable().openapi({
+      example: '张三',
+      description: '真实姓名'
+    }),
+    avatar: z.string().max(255).nullable().openapi({
+      example: 'https://example.com/avatar.jpg',
+      description: '用户头像'
+    }),
+    isDisabled: z.coerce.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
+      example: DisabledStatus.ENABLED,
+      description: '是否禁用(0:启用,1:禁用)'
+    }),
+    defaultDepartmentId: z.coerce.number().int().positive().nullable().openapi({ description: '默认部门ID', example: 1 }),
+    dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL })
+  });
+  
+  export const UpdateUserDto = z.object({
+    username: z.string().min(3).max(255).optional().openapi({
+      example: 'admin',
+      description: '用户名,3-255个字符'
+    }),
+    password: z.string().min(6).max(255).optional().openapi({
+      example: 'password123',
+      description: '密码,最少6位'
+    }),
+    phone: z.string().max(255).nullable().optional().openapi({
+      example: '13800138000',
+      description: '手机号'
+    }),
+    email: z.string().email().max(255).nullable().optional().openapi({
+      example: 'user@example.com',
+      description: '邮箱'
+    }),
+    nickname: z.string().max(255).nullable().optional().openapi({
+      example: '昵称',
+      description: '用户昵称'
+    }),
+    name: z.string().max(255).nullable().optional().openapi({
+      example: '张三',
+      description: '真实姓名'
+    }),
+    avatar: z.string().max(255).nullable().optional().openapi({
+      example: 'https://example.com/avatar.jpg',
+      description: '用户头像'
+    }),
+    isDisabled: z.coerce.number().int().min(0).max(1).optional().openapi({
+      example: DisabledStatus.ENABLED,
+      description: '是否禁用(0:启用,1:禁用)'
+    }),
+    defaultDepartmentId: z.coerce.number().int().positive().nullable().optional().openapi({ description: '默认部门ID', example: 1 }),
+    dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).optional().openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL })
+  });

+ 0 - 125
src/server/modules/users/user.entity.ts

@@ -67,128 +67,3 @@ export class UserEntity {
   }
 }
 
-export const UserSchema = z.object({
-  id: z.number().int().positive().openapi({ description: '用户ID' }),
-  username: z.string().min(3).max(255).openapi({
-    example: 'admin',
-    description: '用户名,3-255个字符'
-  }),
-  password: z.string().min(6).max(255).openapi({
-    example: 'password123',
-    description: '密码,最少6位'
-  }),
-  phone: z.string().max(255).nullable().openapi({
-    example: '13800138000',
-    description: '手机号'
-  }),
-  email: z.string().email().max(255).nullable().openapi({
-    example: 'user@example.com',
-    description: '邮箱'
-  }),
-  nickname: z.string().max(255).nullable().openapi({
-    example: '昵称',
-    description: '用户昵称'
-  }),
-  name: z.string().max(255).nullable().openapi({
-    example: '张三',
-    description: '真实姓名'
-  }),
-  avatar: z.string().max(255).nullable().openapi({
-    example: 'https://example.com/avatar.jpg',
-    description: '用户头像'
-  }),
-  isDisabled: z.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
-    example: DisabledStatus.ENABLED,
-    description: '是否禁用(0:启用,1:禁用)'
-  }),
-  isDeleted: z.number().int().min(0).max(1).default(DeleteStatus.NOT_DELETED).openapi({
-    example: DeleteStatus.NOT_DELETED,
-    description: '是否删除(0:未删除,1:已删除)'
-  }),
-  roles: z.array(RoleSchema).optional().openapi({
-    example: [
-      { id: 1, name: 'admin',description:'管理员' ,createdAt: new Date(), updatedAt: new Date() }
-    ],
-    description: '用户角色列表'
-  }),
-  defaultDepartmentId: z.number().int().positive().nullable().openapi({ description: '默认部门ID', example: 1 }),
-  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL }),
-  createdAt: z.date().openapi({ description: '创建时间' }),
-  updatedAt: z.date().openapi({ description: '更新时间' }),
-  createdBy: z.number().int().positive().nullable().openapi({ description: '创建用户ID', example: 1 }),
-  updatedBy: z.number().int().positive().nullable().openapi({ description: '更新用户ID', example: 1 })
-});
-
-export const CreateUserDto = z.object({
-  username: z.string().min(3).max(255).openapi({
-    example: 'admin',
-    description: '用户名,3-255个字符'
-  }),
-  password: z.string().min(6).max(255).openapi({
-    example: 'password123',
-    description: '密码,最少6位'
-  }),
-  phone: z.string().max(255).nullable().openapi({
-    example: '13800138000',
-    description: '手机号'
-  }),
-  email: z.string().email().max(255).nullable().openapi({
-    example: 'user@example.com',
-    description: '邮箱'
-  }),
-  nickname: z.string().max(255).nullable().openapi({
-    example: '昵称',
-    description: '用户昵称'
-  }),
-  name: z.string().max(255).nullable().openapi({
-    example: '张三',
-    description: '真实姓名'
-  }),
-  avatar: z.string().max(255).nullable().openapi({
-    example: 'https://example.com/avatar.jpg',
-    description: '用户头像'
-  }),
-  isDisabled: z.coerce.number().int().min(0).max(1).default(DisabledStatus.ENABLED).openapi({
-    example: DisabledStatus.ENABLED,
-    description: '是否禁用(0:启用,1:禁用)'
-  }),
-  defaultDepartmentId: z.coerce.number().int().positive().nullable().openapi({ description: '默认部门ID', example: 1 }),
-  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).default(DataScopeType.PERSONAL).openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL })
-});
-
-export const UpdateUserDto = z.object({
-  username: z.string().min(3).max(255).optional().openapi({
-    example: 'admin',
-    description: '用户名,3-255个字符'
-  }),
-  password: z.string().min(6).max(255).optional().openapi({
-    example: 'password123',
-    description: '密码,最少6位'
-  }),
-  phone: z.string().max(255).nullable().optional().openapi({
-    example: '13800138000',
-    description: '手机号'
-  }),
-  email: z.string().email().max(255).nullable().optional().openapi({
-    example: 'user@example.com',
-    description: '邮箱'
-  }),
-  nickname: z.string().max(255).nullable().optional().openapi({
-    example: '昵称',
-    description: '用户昵称'
-  }),
-  name: z.string().max(255).nullable().optional().openapi({
-    example: '张三',
-    description: '真实姓名'
-  }),
-  avatar: z.string().max(255).nullable().optional().openapi({
-    example: 'https://example.com/avatar.jpg',
-    description: '用户头像'
-  }),
-  isDisabled: z.coerce.number().int().min(0).max(1).optional().openapi({
-    example: DisabledStatus.ENABLED,
-    description: '是否禁用(0:启用,1:禁用)'
-  }),
-  defaultDepartmentId: z.coerce.number().int().positive().nullable().optional().openapi({ description: '默认部门ID', example: 1 }),
-  dataScopeType: z.enum([DataScopeType.PERSONAL, DataScopeType.DEPARTMENT, DataScopeType.SUB_DEPARTMENT, DataScopeType.COMPANY, DataScopeType.CUSTOM]).optional().openapi({ description: '数据范围类型', example: DataScopeType.PERSONAL })
-});

+ 70 - 0
test-permission-fix.ts

@@ -0,0 +1,70 @@
+#!/usr/bin/env node
+/**
+ * 权限中间件修复测试脚本
+ * 用于验证权限匹配逻辑是否正确
+ */
+
+import { UserEntity } from './src/server/modules/users/user.entity';
+import { Role } from './src/server/modules/users/role.entity';
+import { Permission } from './src/server/modules/permissions/permission.entity';
+import { RolePermission } from './src/server/modules/permissions/permission.entity';
+import { checkPermission } from './src/server/middleware/permission.middleware';
+
+// 模拟用户数据
+function createTestUser(): UserEntity {
+  const user = new UserEntity();
+  user.id = 1;
+  user.username = 'testuser';
+  
+  // 创建管理员角色
+  const adminRole = new Role();
+  adminRole.id = 1;
+  adminRole.name = '管理员';
+  
+  // 创建创建用户权限
+  const createUserPermission = new Permission();
+  createUserPermission.id = 1;
+  createUserPermission.code = 'system:user:create';
+  createUserPermission.name = '创建用户';
+  
+  // 创建角色权限关联
+  const rolePermission = new RolePermission();
+  rolePermission.id = 1;
+  rolePermission.role = adminRole;
+  rolePermission.permission = createUserPermission;
+  
+  // 关联角色和权限
+  adminRole.rolePermissions = [rolePermission];
+  user.roles = [adminRole];
+  
+  return user;
+}
+
+// 测试权限检查
+function testPermissionCheck() {
+  const user = createTestUser();
+  const permissionChecker = checkPermission(['system:user:create']);
+  
+  console.log('=== 权限中间件修复测试 ===');
+  console.log('测试用户拥有权限:', 'system:user:create');
+  console.log('检查所需权限:', ['system:user:create']);
+  
+  const result = permissionChecker(user);
+  console.log('权限检查结果:', result ? '✅ 通过' : '❌ 失败');
+  
+  // 测试不存在的权限
+  const noPermissionChecker = checkPermission(['system:user:delete']);
+  const noResult = noPermissionChecker(user);
+  console.log('检查未拥有权限:', ['system:user:delete']);
+  console.log('权限检查结果:', !noResult ? '✅ 正确拒绝' : '❌ 错误通过');
+  
+  console.log('\n=== 测试总结 ===');
+  console.log('修复后的权限中间件已经能够正确匹配权限编码!');
+}
+
+// 运行测试
+if (require.main === module) {
+  testPermissionCheck();
+}
+
+export { testPermissionCheck };