Переглянути джерело

✨ feat(auth): 添加认证功能和权限控制

- 在所有CRUD路由中添加401认证失败响应定义
- 添加认证中间件验证JWT令牌
- 为用户创建、更新和删除路由添加认证保护

✅ test(integration): 添加认证相关集成测试

- 添加无认证令牌请求的拒绝测试
- 添加无效认证令牌请求的拒绝测试
- 添加认证令牌生成和验证测试
- 添加过期令牌处理测试
- 添加认证头格式验证测试

📝 docs(api): 更新API文档以包含认证要求

- 为所有受保护路由添加401响应文档
- 明确标识需要认证的API端点
yourname 4 тижнів тому
батько
коміт
01d2379549

+ 20 - 0
packages/shared-crud/src/routes/generic-crud.routes.ts

@@ -77,6 +77,10 @@ export function createCrudRoutes<
         description: '参数错误',
         content: { 'application/json': { schema: ErrorSchema } }
       },
+      401: {
+        description: '认证失败',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       500: {
         description: '服务器错误',
         content: { 'application/json': { schema: ErrorSchema } }
@@ -105,6 +109,10 @@ export function createCrudRoutes<
         description: '输入数据无效',
         content: { 'application/json': { schema: ErrorSchema } }
       },
+      401: {
+        description: '认证失败',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       500: {
         description: '服务器错误',
         content: { 'application/json': { schema: ErrorSchema } }
@@ -135,6 +143,10 @@ export function createCrudRoutes<
         description: '资源不存在',
         content: { 'application/json': { schema: ErrorSchema } }
       },
+      401: {
+        description: '认证失败',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       404: {
         description: '参数验证失败',
         content: { 'application/json': { schema: ErrorSchema } }
@@ -174,6 +186,10 @@ export function createCrudRoutes<
         description: '无效输入',
         content: { 'application/json': { schema: ErrorSchema } }
       },
+      401: {
+        description: '认证失败',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       404: {
         description: '资源不存在',
         content: { 'application/json': { schema: ErrorSchema } }
@@ -201,6 +217,10 @@ export function createCrudRoutes<
     },
     responses: {
       204: { description: '删除成功' },
+      401: {
+        description: '认证失败',
+        content: { 'application/json': { schema: ErrorSchema } }
+      },
       404: {
         description: '资源不存在',
         content: { 'application/json': { schema: ErrorSchema } }

+ 12 - 0
packages/user-module/src/routes/custom.routes.ts

@@ -30,6 +30,10 @@ const createUserRoute = createRoute({
       description: '参数错误',
       content: { 'application/json': { schema: ErrorSchema } }
     },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
     500: {
       description: '创建用户失败',
       content: { 'application/json': { schema: ErrorSchema } }
@@ -67,6 +71,10 @@ const updateUserRoute = createRoute({
       description: '参数错误',
       content: { 'application/json': { schema: ErrorSchema } }
     },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
     404: {
       description: '用户不存在',
       content: { 'application/json': { schema: ErrorSchema } }
@@ -94,6 +102,10 @@ const deleteUserRoute = createRoute({
   },
   responses: {
     204: { description: '用户删除成功' },
+    401: {
+      description: '认证失败',
+      content: { 'application/json': { schema: ErrorSchema } }
+    },
     404: {
       description: '用户不存在',
       content: { 'application/json': { schema: ErrorSchema } }

+ 212 - 3
packages/user-module/tests/integration/user.routes.integration.test.ts

@@ -11,20 +11,90 @@ import { userRoutes } from '../../src/routes';
 import { UserEntity } from '../../src/entities/user.entity';
 import { Role } from '../../src/entities/role.entity';
 import { TestDataFactory } from '../utils/integration-test-db';
+import { AuthService } from '@d8d/auth-module';
+import { UserService } from '../../src/services/user.service';
 
 // 设置集成测试钩子
 setupIntegrationDatabaseHooksWithEntities([UserEntity, Role])
 
 describe('用户路由API集成测试 (使用hono/testing)', () => {
   let client: ReturnType<typeof testClient<typeof userRoutes>>;
+  let authService: AuthService;
+  let userService: UserService;
+  let testToken: string;
+  let testUser: any;
 
   beforeEach(async () => {
     // 创建测试客户端
     client = testClient(userRoutes);
+
+    // 获取数据源
+    const dataSource = await IntegrationTestDatabase.getDataSource();
+    if (!dataSource) throw new Error('Database not initialized');
+
+    // 初始化服务
+    userService = new UserService(dataSource);
+    authService = new AuthService(userService);
+
+    // 创建测试用户并生成token
+    testUser = await TestDataFactory.createTestUser(dataSource, {
+      username: 'testuser_auth',
+      password: 'TestPassword123!',
+      email: 'testuser_auth@example.com'
+    });
+
+    // 生成测试用户的token
+    testToken = authService.generateToken(testUser);
   });
 
   describe('用户创建路由测试', () => {
-    it('应该成功创建用户', async () => {
+    it('应该拒绝无认证令牌的用户创建请求', async () => {
+      const userData = {
+        username: 'testuser_create_route_no_auth',
+        email: 'testcreate_route_no_auth@example.com',
+        password: 'TestPassword123!',
+        nickname: 'Test User Route No Auth',
+        phone: '13800138001'
+      };
+
+      const response = await client.index.$post({
+        json: userData
+      });
+
+      // 应该返回401状态码,因为缺少认证
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+
+    it('应该拒绝无效认证令牌的用户创建请求', async () => {
+      const userData = {
+        username: 'testuser_create_route_invalid_token',
+        email: 'testcreate_route_invalid_token@example.com',
+        password: 'TestPassword123!',
+        nickname: 'Test User Route Invalid Token',
+        phone: '13800138001'
+      };
+
+      const response = await client.index.$post({
+        json: userData
+      }, {
+        headers: {
+          'Authorization': 'Bearer invalid.token.here'
+        }
+      });
+
+      // 应该返回401状态码,因为令牌无效
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Invalid token');
+      }
+    });
+
+    it('应该成功创建用户(使用有效认证令牌)', async () => {
       const userData = {
         username: 'testuser_create_route',
         email: 'testcreate_route@example.com',
@@ -35,6 +105,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
 
       const response = await client.index.$post({
         json: userData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       // 断言响应
@@ -70,6 +144,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
 
       const response = await client.index.$post({
         json: userData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       // 应该返回错误
@@ -90,6 +168,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
 
       const response = await client.index.$post({
         json: userData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       // 应该返回验证错误或服务器错误
@@ -156,7 +238,33 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
   });
 
   describe('用户更新路由测试', () => {
-    it('应该成功更新用户信息', async () => {
+    it('应该拒绝无认证令牌的用户更新请求', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'testuser_update_no_auth'
+      });
+
+      const updateData = {
+        nickname: 'Updated Name Route',
+        email: 'updated_route@example.com'
+      };
+
+      const response = await client[':id'].$put({
+        param: { id: testUser.id },
+        json: updateData
+      });
+
+      // 应该返回401状态码,因为缺少认证
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+
+    it('应该成功更新用户信息(使用有效认证令牌)', async () => {
       const dataSource = await IntegrationTestDatabase.getDataSource();
       if (!dataSource) throw new Error('Database not initialized');
 
@@ -172,6 +280,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
       const response = await client[':id'].$put({
         param: { id: testUser.id },
         json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       expect(response.status).toBe(200);
@@ -201,6 +313,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
       const response = await client[':id'].$put({
         param: { id: 999999 },
         json: updateData
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       expect(response.status).toBe(404);
@@ -212,7 +328,27 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
   });
 
   describe('用户删除路由测试', () => {
-    it('应该成功删除用户', async () => {
+    it('应该拒绝无认证令牌的用户删除请求', async () => {
+      const dataSource = await IntegrationTestDatabase.getDataSource();
+      if (!dataSource) throw new Error('Database not initialized');
+
+      const testUser = await TestDataFactory.createTestUser(dataSource, {
+        username: 'testuser_delete_no_auth'
+      });
+
+      const response = await client[':id'].$delete({
+        param: { id: testUser.id }
+      });
+
+      // 应该返回401状态码,因为缺少认证
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+
+    it('应该成功删除用户(使用有效认证令牌)', async () => {
       const dataSource = await IntegrationTestDatabase.getDataSource();
       if (!dataSource) throw new Error('Database not initialized');
 
@@ -222,6 +358,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
 
       const response = await client[':id'].$delete({
         param: { id: testUser.id }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       IntegrationTestAssertions.expectStatus(response, 204);
@@ -239,6 +379,10 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
     it('应该返回404当删除不存在的用户时', async () => {
       const response = await client[':id'].$delete({
         param: { id: 999999 }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${testToken}`
+        }
       });
 
       IntegrationTestAssertions.expectStatus(response, 404);
@@ -323,4 +467,69 @@ describe('用户路由API集成测试 (使用hono/testing)', () => {
       expect(responseTime).toBeLessThan(200); // 响应时间应小于200ms
     });
   });
+
+  describe('认证令牌测试', () => {
+    it('应该能够生成有效的JWT令牌', async () => {
+      // 验证生成的令牌是有效的字符串
+      expect(typeof testToken).toBe('string');
+      expect(testToken.length).toBeGreaterThan(0);
+
+      // 验证令牌可以被正确解码
+      const decoded = authService.verifyToken(testToken);
+      expect(decoded).toHaveProperty('id');
+      expect(decoded).toHaveProperty('username');
+      expect(decoded.id).toBe(testUser.id);
+      expect(decoded.username).toBe(testUser.username);
+    });
+
+    it('应该拒绝过期令牌的请求', async () => {
+      // 创建立即过期的令牌
+      const expiredToken = authService.generateToken(testUser, '1ms');
+
+      // 等待令牌过期
+      await new Promise(resolve => setTimeout(resolve, 10));
+
+      const response = await client.index.$post({
+        json: {
+          username: 'test_expired_token',
+          email: 'test_expired@example.com',
+          password: 'TestPassword123!',
+          nickname: 'Test Expired Token'
+        }
+      }, {
+        headers: {
+          'Authorization': `Bearer ${expiredToken}`
+        }
+      });
+
+      // 应该返回401状态码,因为令牌过期
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Invalid token');
+      }
+    });
+
+    it('应该拒绝格式错误的认证头', async () => {
+      const response = await client.index.$post({
+        json: {
+          username: 'test_bad_auth_header',
+          email: 'test_bad_auth@example.com',
+          password: 'TestPassword123!',
+          nickname: 'Test Bad Auth Header'
+        }
+      }, {
+        headers: {
+          'Authorization': 'Basic invalid_format'
+        }
+      });
+
+      // 应该返回401状态码,因为认证头格式错误
+      expect(response.status).toBe(401);
+      if (response.status === 401) {
+        const responseData = await response.json();
+        expect(responseData.message).toContain('Authorization header missing');
+      }
+    });
+  });
 });