From 470b0b8dbf0466ff663a892c0fe2d96346d62f50 Mon Sep 17 00:00:00 2001 From: moyin <244344649@qq.com> Date: Tue, 6 Jan 2026 15:17:05 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0JWT=E8=AE=A4=E8=AF=81?= =?UTF-8?q?=E7=B3=BB=E7=BB=9F=E5=92=8CZulip=E7=94=A8=E6=88=B7=E7=AE=A1?= =?UTF-8?q?=E7=90=86=E6=9C=8D=E5=8A=A1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增JWT认证守卫(JwtAuthGuard)和当前用户装饰器(CurrentUser) - 添加JWT使用示例和完整的认证流程文档 - 实现Zulip用户管理服务,支持用户查询、验证和管理 - 实现Zulip用户注册服务,支持新用户创建和注册流程 - 添加完整的单元测试覆盖 - 新增真实环境测试脚本,验证Zulip API集成 - 更新.gitignore,排除.kiro目录 主要功能: - JWT令牌验证和用户信息提取 - 用户存在性检查和信息获取 - Zulip API集成和错误处理 - 完整的测试覆盖和文档 --- .gitignore | 2 + .../auth/decorators/current-user.decorator.ts | 39 ++ .../auth/examples/jwt-usage-example.ts | 128 +++++ src/business/auth/guards/jwt-auth.guard.ts | 83 +++ .../services/user_management.service.spec.ts | 388 +++++++++++++ .../zulip/services/user_management.service.ts | 539 ++++++++++++++++++ .../user_registration.service.spec.ts | 188 ++++++ .../services/user_registration.service.ts | 531 +++++++++++++++++ test_zulip_registration.js | 196 +++++++ test_zulip_user_management.js | 275 +++++++++ 10 files changed, 2369 insertions(+) create mode 100644 src/business/auth/decorators/current-user.decorator.ts create mode 100644 src/business/auth/examples/jwt-usage-example.ts create mode 100644 src/business/auth/guards/jwt-auth.guard.ts create mode 100644 src/core/zulip/services/user_management.service.spec.ts create mode 100644 src/core/zulip/services/user_management.service.ts create mode 100644 src/core/zulip/services/user_registration.service.spec.ts create mode 100644 src/core/zulip/services/user_registration.service.ts create mode 100644 test_zulip_registration.js create mode 100644 test_zulip_user_management.js diff --git a/.gitignore b/.gitignore index 1eae1a2..1a93f86 100644 --- a/.gitignore +++ b/.gitignore @@ -44,3 +44,5 @@ coverage/ # Redis数据文件(本地开发用) redis-data/ + +.kiro/ \ No newline at end of file diff --git a/src/business/auth/decorators/current-user.decorator.ts b/src/business/auth/decorators/current-user.decorator.ts new file mode 100644 index 0000000..ae00731 --- /dev/null +++ b/src/business/auth/decorators/current-user.decorator.ts @@ -0,0 +1,39 @@ +/** + * 当前用户装饰器 + * + * 功能描述: + * - 从请求上下文中提取当前认证用户信息 + * - 简化控制器中获取用户信息的操作 + * + * 使用示例: + * ```typescript + * @Get('profile') + * @UseGuards(JwtAuthGuard) + * getProfile(@CurrentUser() user: JwtPayload) { + * return { user }; + * } + * ``` + * + * @author kiro-ai + * @version 1.0.0 + * @since 2025-01-05 + */ + +import { createParamDecorator, ExecutionContext } from '@nestjs/common'; +import { AuthenticatedRequest, JwtPayload } from '../guards/jwt-auth.guard'; + +/** + * 当前用户装饰器 + * + * @param data 可选的属性名,用于获取用户对象的特定属性 + * @param ctx 执行上下文 + * @returns 用户信息或用户的特定属性 + */ +export const CurrentUser = createParamDecorator( + (data: keyof JwtPayload | undefined, ctx: ExecutionContext) => { + const request = ctx.switchToHttp().getRequest(); + const user = request.user; + + return data ? user?.[data] : user; + }, +); \ No newline at end of file diff --git a/src/business/auth/examples/jwt-usage-example.ts b/src/business/auth/examples/jwt-usage-example.ts new file mode 100644 index 0000000..09c694f --- /dev/null +++ b/src/business/auth/examples/jwt-usage-example.ts @@ -0,0 +1,128 @@ +/** + * JWT 使用示例 + * + * 展示如何在控制器中使用 JWT 认证守卫和当前用户装饰器 + * + * @author kiro-ai + * @version 1.0.0 + * @since 2025-01-05 + */ + +import { Controller, Get, UseGuards, Post, Body } from '@nestjs/common'; +import { JwtAuthGuard, JwtPayload } from '../guards/jwt-auth.guard'; +import { CurrentUser } from '../decorators/current-user.decorator'; + +/** + * 示例控制器 - 展示 JWT 认证的使用方法 + */ +@Controller('example') +export class ExampleController { + + /** + * 公开接口 - 无需认证 + */ + @Get('public') + getPublicData() { + return { + message: '这是一个公开接口,无需认证', + timestamp: new Date().toISOString(), + }; + } + + /** + * 受保护的接口 - 需要 JWT 认证 + * + * 请求头示例: + * Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9... + */ + @Get('protected') + @UseGuards(JwtAuthGuard) + getProtectedData(@CurrentUser() user: JwtPayload) { + return { + message: '这是一个受保护的接口,需要有效的 JWT 令牌', + user: { + id: user.sub, + username: user.username, + role: user.role, + }, + timestamp: new Date().toISOString(), + }; + } + + /** + * 获取当前用户信息 + */ + @Get('profile') + @UseGuards(JwtAuthGuard) + getUserProfile(@CurrentUser() user: JwtPayload) { + return { + profile: { + userId: user.sub, + username: user.username, + role: user.role, + tokenIssuedAt: new Date(user.iat * 1000).toISOString(), + tokenExpiresAt: new Date(user.exp * 1000).toISOString(), + }, + }; + } + + /** + * 获取用户的特定属性 + */ + @Get('username') + @UseGuards(JwtAuthGuard) + getUsername(@CurrentUser('username') username: string) { + return { + username, + message: `你好,${username}!`, + }; + } + + /** + * 需要特定角色的接口 + */ + @Post('admin-only') + @UseGuards(JwtAuthGuard) + adminOnlyAction(@CurrentUser() user: JwtPayload, @Body() data: any) { + // 检查用户角色 + if (user.role !== 1) { // 假设 1 是管理员角色 + return { + success: false, + message: '权限不足,仅管理员可访问', + }; + } + + return { + success: true, + message: '管理员操作执行成功', + data, + operator: user.username, + }; + } +} + +/** + * 使用说明: + * + * 1. 首先调用登录接口获取 JWT 令牌: + * POST /auth/login + * { + * "identifier": "username", + * "password": "password" + * } + * + * 2. 从响应中获取 access_token + * + * 3. 在后续请求中添加 Authorization 头: + * Authorization: Bearer + * + * 4. 访问受保护的接口: + * GET /example/protected + * GET /example/profile + * GET /example/username + * POST /example/admin-only + * + * 错误处理: + * - 401 Unauthorized: 令牌缺失或无效 + * - 403 Forbidden: 令牌有效但权限不足 + */ \ No newline at end of file diff --git a/src/business/auth/guards/jwt-auth.guard.ts b/src/business/auth/guards/jwt-auth.guard.ts new file mode 100644 index 0000000..9715223 --- /dev/null +++ b/src/business/auth/guards/jwt-auth.guard.ts @@ -0,0 +1,83 @@ +/** + * JWT 认证守卫 + * + * 功能描述: + * - 验证请求中的 JWT 令牌 + * - 提取用户信息并添加到请求上下文 + * - 保护需要认证的路由 + * + * @author kiro-ai + * @version 1.0.0 + * @since 2025-01-05 + */ + +import { + Injectable, + CanActivate, + ExecutionContext, + UnauthorizedException, + Logger, +} from '@nestjs/common'; +import { JwtService } from '@nestjs/jwt'; +import { Request } from 'express'; + +/** + * JWT 载荷接口 + */ +export interface JwtPayload { + sub: string; // 用户ID + username: string; + role: number; + iat: number; // 签发时间 + exp: number; // 过期时间 +} + +/** + * 扩展的请求接口,包含用户信息 + */ +export interface AuthenticatedRequest extends Request { + user: JwtPayload; +} + +@Injectable() +export class JwtAuthGuard implements CanActivate { + private readonly logger = new Logger(JwtAuthGuard.name); + + constructor(private readonly jwtService: JwtService) {} + + async canActivate(context: ExecutionContext): Promise { + const request = context.switchToHttp().getRequest(); + const token = this.extractTokenFromHeader(request); + + if (!token) { + this.logger.warn('访问被拒绝:缺少认证令牌'); + throw new UnauthorizedException('缺少认证令牌'); + } + + try { + // 验证并解码 JWT 令牌 + const payload = await this.jwtService.verifyAsync(token); + + // 将用户信息添加到请求对象 + (request as AuthenticatedRequest).user = payload; + + this.logger.log(`用户认证成功: ${payload.username} (ID: ${payload.sub})`); + return true; + } catch (error) { + const errorMessage = error instanceof Error ? error.message : '未知错误'; + this.logger.warn(`JWT 令牌验证失败: ${errorMessage}`); + throw new UnauthorizedException('无效的认证令牌'); + } + } + + /** + * 从请求头中提取 JWT 令牌 + * + * @param request 请求对象 + * @returns JWT 令牌或 undefined + */ + private extractTokenFromHeader(request: Request): string | undefined { + const [type, token] = request.headers.authorization?.split(' ') ?? []; + return type === 'Bearer' ? token : undefined; + } +} \ No newline at end of file diff --git a/src/core/zulip/services/user_management.service.spec.ts b/src/core/zulip/services/user_management.service.spec.ts new file mode 100644 index 0000000..f99e2f5 --- /dev/null +++ b/src/core/zulip/services/user_management.service.spec.ts @@ -0,0 +1,388 @@ +/** + * Zulip用户管理服务测试 + * + * 功能描述: + * - 测试UserManagementService的核心功能 + * - 测试用户查询和验证逻辑 + * - 测试错误处理和边界情况 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-06 + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { UserManagementService, UserQueryRequest, UserValidationRequest } from './user_management.service'; +import { IZulipConfigService } from '../interfaces/zulip-core.interfaces'; + +// 模拟fetch +global.fetch = jest.fn(); + +describe('UserManagementService', () => { + let service: UserManagementService; + let mockConfigService: jest.Mocked; + let mockFetch: jest.MockedFunction; + + beforeEach(async () => { + // 重置fetch模拟 + mockFetch = fetch as jest.MockedFunction; + mockFetch.mockClear(); + + // 创建模拟的配置服务 + mockConfigService = { + getZulipConfig: jest.fn().mockReturnValue({ + zulipServerUrl: 'https://test.zulip.com', + zulipBotEmail: 'bot@test.com', + zulipBotApiKey: 'test-api-key', + }), + getMapIdByStream: jest.fn(), + getStreamByMap: jest.fn(), + getMapConfig: jest.fn(), + hasMap: jest.fn(), + getAllMapIds: jest.fn(), + getMapConfigByStream: jest.fn(), + getAllStreams: jest.fn(), + hasStream: jest.fn(), + findObjectByTopic: jest.fn(), + getObjectsInMap: jest.fn(), + getTopicByObject: jest.fn(), + findNearbyObject: jest.fn(), + reloadConfig: jest.fn(), + validateConfig: jest.fn(), + getAllMapConfigs: jest.fn(), + getConfigStats: jest.fn(), + getConfigFilePath: jest.fn(), + configFileExists: jest.fn(), + enableConfigWatcher: jest.fn(), + disableConfigWatcher: jest.fn(), + isConfigWatcherEnabled: jest.fn(), + getFullConfiguration: jest.fn(), + updateConfigValue: jest.fn(), + exportMapConfig: jest.fn(), + } as jest.Mocked; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserManagementService, + { + provide: 'ZULIP_CONFIG_SERVICE', + useValue: mockConfigService, + }, + ], + }).compile(); + + service = module.get(UserManagementService); + }); + + it('应该正确初始化服务', () => { + expect(service).toBeDefined(); + }); + + describe('checkUserExists - 检查用户是否存在', () => { + it('应该正确检查存在的用户', async () => { + // 模拟API响应 + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [ + { + user_id: 1, + email: 'test@example.com', + full_name: 'Test User', + is_active: true, + is_admin: false, + is_bot: false, + }, + ], + }), + } as Response); + + const result = await service.checkUserExists('test@example.com'); + + expect(result).toBe(true); + expect(mockFetch).toHaveBeenCalledWith( + 'https://test.zulip.com/api/v1/users', + expect.objectContaining({ + method: 'GET', + headers: expect.objectContaining({ + 'Authorization': expect.stringContaining('Basic'), + }), + }) + ); + }); + + it('应该正确检查不存在的用户', async () => { + // 模拟API响应 + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [ + { + user_id: 1, + email: 'other@example.com', + full_name: 'Other User', + is_active: true, + is_admin: false, + is_bot: false, + }, + ], + }), + } as Response); + + const result = await service.checkUserExists('test@example.com'); + + expect(result).toBe(false); + }); + + it('应该处理无效邮箱', async () => { + const result = await service.checkUserExists('invalid-email'); + + expect(result).toBe(false); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('应该处理API调用失败', async () => { + // 模拟API失败 + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 500, + statusText: 'Internal Server Error', + } as Response); + + const result = await service.checkUserExists('test@example.com'); + + expect(result).toBe(false); + }); + + it('应该处理网络异常', async () => { + // 模拟网络异常 + mockFetch.mockRejectedValueOnce(new Error('Network error')); + + const result = await service.checkUserExists('test@example.com'); + + expect(result).toBe(false); + }); + }); + + describe('getUserInfo - 获取用户信息', () => { + it('应该成功获取用户信息', async () => { + const request: UserQueryRequest = { + email: 'test@example.com', + }; + + // 模拟API响应 + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [ + { + user_id: 1, + email: 'test@example.com', + full_name: 'Test User', + is_active: true, + is_admin: false, + is_bot: false, + }, + ], + }), + } as Response); + + const result = await service.getUserInfo(request); + + expect(result.success).toBe(true); + expect(result.userId).toBe(1); + expect(result.email).toBe('test@example.com'); + expect(result.fullName).toBe('Test User'); + expect(result.isActive).toBe(true); + expect(result.isAdmin).toBe(false); + expect(result.isBot).toBe(false); + }); + + it('应该处理用户不存在的情况', async () => { + const request: UserQueryRequest = { + email: 'nonexistent@example.com', + }; + + // 模拟API响应 + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [], + }), + } as Response); + + const result = await service.getUserInfo(request); + + expect(result.success).toBe(false); + expect(result.error).toBe('用户不存在'); + }); + + it('应该拒绝无效邮箱', async () => { + const request: UserQueryRequest = { + email: 'invalid-email', + }; + + const result = await service.getUserInfo(request); + + expect(result.success).toBe(false); + expect(result.error).toBe('邮箱格式无效'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + + describe('validateUserCredentials - 验证用户凭据', () => { + it('应该成功验证有效的API Key', async () => { + const request: UserValidationRequest = { + email: 'test@example.com', + apiKey: 'valid-api-key', + }; + + // 模拟API Key验证响应(第一个调用) + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({}), + } as Response); + + // 模拟用户列表API响应(第二个调用) + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [ + { + user_id: 1, + email: 'test@example.com', + full_name: 'Test User', + is_active: true, + is_admin: false, + is_bot: false, + }, + ], + }), + } as Response); + + const result = await service.validateUserCredentials(request); + + expect(result.success).toBe(true); + expect(result.isValid).toBe(true); + expect(result.userId).toBe(1); + }); + + it('应该拒绝无效的API Key', async () => { + const request: UserValidationRequest = { + email: 'test@example.com', + apiKey: 'invalid-api-key', + }; + + // 模拟API Key验证失败(第一个调用) + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 401, + statusText: 'Unauthorized', + } as Response); + + const result = await service.validateUserCredentials(request); + + expect(result.success).toBe(true); + expect(result.isValid).toBe(false); + }); + + it('应该拒绝空的API Key', async () => { + const request: UserValidationRequest = { + email: 'test@example.com', + apiKey: '', + }; + + const result = await service.validateUserCredentials(request); + + expect(result.success).toBe(false); + expect(result.error).toBe('API Key不能为空'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + + it('应该拒绝无效邮箱', async () => { + const request: UserValidationRequest = { + email: 'invalid-email', + apiKey: 'some-api-key', + }; + + const result = await service.validateUserCredentials(request); + + expect(result.success).toBe(false); + expect(result.error).toBe('邮箱格式无效'); + expect(mockFetch).not.toHaveBeenCalled(); + }); + }); + + describe('getAllUsers - 获取所有用户', () => { + it('应该成功获取用户列表', async () => { + // 模拟API响应 + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [ + { + user_id: 1, + email: 'user1@example.com', + full_name: 'User One', + is_active: true, + is_admin: false, + is_bot: false, + }, + { + user_id: 2, + email: 'user2@example.com', + full_name: 'User Two', + is_active: true, + is_admin: true, + is_bot: false, + }, + ], + }), + } as Response); + + const result = await service.getAllUsers(); + + expect(result.success).toBe(true); + expect(result.users).toHaveLength(2); + expect(result.totalCount).toBe(2); + expect(result.users?.[0]).toEqual({ + userId: 1, + email: 'user1@example.com', + fullName: 'User One', + isActive: true, + isAdmin: false, + isBot: false, + }); + }); + + it('应该处理空用户列表', async () => { + // 模拟API响应 + mockFetch.mockResolvedValueOnce({ + ok: true, + json: async () => ({ + members: [], + }), + } as Response); + + const result = await service.getAllUsers(); + + expect(result.success).toBe(true); + expect(result.users).toHaveLength(0); + expect(result.totalCount).toBe(0); + }); + + it('应该处理API调用失败', async () => { + // 模拟API失败 + mockFetch.mockResolvedValueOnce({ + ok: false, + status: 403, + statusText: 'Forbidden', + } as Response); + + const result = await service.getAllUsers(); + + expect(result.success).toBe(false); + expect(result.error).toContain('API调用失败'); + }); + }); +}); \ No newline at end of file diff --git a/src/core/zulip/services/user_management.service.ts b/src/core/zulip/services/user_management.service.ts new file mode 100644 index 0000000..7dd81c0 --- /dev/null +++ b/src/core/zulip/services/user_management.service.ts @@ -0,0 +1,539 @@ +/** + * Zulip用户管理服务 + * + * 功能描述: + * - 查询和验证Zulip用户信息 + * - 检查用户是否存在 + * - 获取用户详细信息 + * - 验证用户凭据和权限 + * + * 主要方法: + * - checkUserExists(): 检查用户是否存在 + * - getUserInfo(): 获取用户详细信息 + * - validateUserCredentials(): 验证用户凭据 + * - getAllUsers(): 获取所有用户列表 + * + * 使用场景: + * - 用户登录时验证用户存在性 + * - 获取用户基本信息 + * - 验证用户权限和状态 + * - 管理员查看用户列表 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-06 + */ + +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { IZulipConfigService } from '../interfaces/zulip-core.interfaces'; + +/** + * Zulip API响应接口 + */ +interface ZulipApiResponse { + result?: 'success' | 'error'; + msg?: string; + message?: string; +} + +/** + * 用户信息接口 + */ +interface ZulipUser { + user_id: number; + email: string; + full_name: string; + is_active: boolean; + is_admin: boolean; + is_owner: boolean; + is_bot: boolean; + date_joined: string; +} + +/** + * 用户列表响应接口 + */ +interface ZulipUsersResponse extends ZulipApiResponse { + members?: ZulipUser[]; +} + +/** + * 用户查询请求接口 + */ +export interface UserQueryRequest { + email: string; +} + +/** + * 用户信息响应接口 + */ +export interface UserInfoResponse { + success: boolean; + userId?: number; + email?: string; + fullName?: string; + isActive?: boolean; + isAdmin?: boolean; + isBot?: boolean; + dateJoined?: string; + error?: string; +} + +/** + * 用户验证请求接口 + */ +export interface UserValidationRequest { + email: string; + apiKey?: string; +} + +/** + * 用户验证响应接口 + */ +export interface UserValidationResponse { + success: boolean; + isValid?: boolean; + userId?: number; + error?: string; +} + +/** + * 用户列表响应接口 + */ +export interface UsersListResponse { + success: boolean; + users?: Array<{ + userId: number; + email: string; + fullName: string; + isActive: boolean; + isAdmin: boolean; + isBot: boolean; + }>; + totalCount?: number; + error?: string; +} + +/** + * Zulip用户管理服务类 + * + * 职责: + * - 查询和验证Zulip用户信息 + * - 检查用户是否存在于Zulip服务器 + * - 获取用户详细信息和权限状态 + * - 提供用户管理相关的API接口 + */ +@Injectable() +export class UserManagementService { + private readonly logger = new Logger(UserManagementService.name); + + constructor( + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configService: IZulipConfigService, + ) { + this.logger.log('UserManagementService初始化完成'); + } + + /** + * 检查用户是否存在 + * + * 功能描述: + * 通过Zulip API检查指定邮箱的用户是否存在 + * + * 业务逻辑: + * 1. 获取所有用户列表 + * 2. 在列表中查找指定邮箱 + * 3. 返回用户存在性结果 + * + * @param email 用户邮箱 + * @returns Promise 是否存在 + */ + async checkUserExists(email: string): Promise { + const startTime = Date.now(); + + this.logger.log('开始检查用户是否存在', { + operation: 'checkUserExists', + email, + timestamp: new Date().toISOString(), + }); + + try { + // 1. 验证邮箱格式 + if (!email || !this.isValidEmail(email)) { + this.logger.warn('邮箱格式无效', { + operation: 'checkUserExists', + email, + }); + return false; + } + + // 2. 获取用户列表 + const usersResult = await this.getAllUsers(); + if (!usersResult.success) { + this.logger.warn('获取用户列表失败', { + operation: 'checkUserExists', + email, + error: usersResult.error, + }); + return false; + } + + // 3. 检查用户是否存在 + const userExists = usersResult.users?.some(user => + user.email.toLowerCase() === email.toLowerCase() + ) || false; + + const duration = Date.now() - startTime; + + this.logger.log('用户存在性检查完成', { + operation: 'checkUserExists', + email, + exists: userExists, + duration, + timestamp: new Date().toISOString(), + }); + + return userExists; + + } catch (error) { + const err = error as Error; + const duration = Date.now() - startTime; + + this.logger.error('检查用户存在性失败', { + operation: 'checkUserExists', + email, + error: err.message, + duration, + timestamp: new Date().toISOString(), + }, err.stack); + + return false; + } + } + + /** + * 获取用户详细信息 + * + * 功能描述: + * 根据邮箱获取用户的详细信息 + * + * @param request 用户查询请求 + * @returns Promise + */ + async getUserInfo(request: UserQueryRequest): Promise { + const startTime = Date.now(); + + this.logger.log('开始获取用户信息', { + operation: 'getUserInfo', + email: request.email, + timestamp: new Date().toISOString(), + }); + + try { + // 1. 验证请求参数 + if (!request.email || !this.isValidEmail(request.email)) { + return { + success: false, + error: '邮箱格式无效', + }; + } + + // 2. 获取用户列表 + const usersResult = await this.getAllUsers(); + if (!usersResult.success) { + return { + success: false, + error: usersResult.error || '获取用户列表失败', + }; + } + + // 3. 查找指定用户 + const user = usersResult.users?.find(u => + u.email.toLowerCase() === request.email.toLowerCase() + ); + + if (!user) { + return { + success: false, + error: '用户不存在', + }; + } + + const duration = Date.now() - startTime; + + this.logger.log('用户信息获取完成', { + operation: 'getUserInfo', + email: request.email, + userId: user.userId, + duration, + timestamp: new Date().toISOString(), + }); + + return { + success: true, + userId: user.userId, + email: user.email, + fullName: user.fullName, + isActive: user.isActive, + isAdmin: user.isAdmin, + isBot: user.isBot, + }; + + } catch (error) { + const err = error as Error; + const duration = Date.now() - startTime; + + this.logger.error('获取用户信息失败', { + operation: 'getUserInfo', + email: request.email, + error: err.message, + duration, + timestamp: new Date().toISOString(), + }, err.stack); + + return { + success: false, + error: '系统错误,请稍后重试', + }; + } + } + + /** + * 验证用户凭据 + * + * 功能描述: + * 验证用户的API Key是否有效 + * + * @param request 用户验证请求 + * @returns Promise + */ + async validateUserCredentials(request: UserValidationRequest): Promise { + const startTime = Date.now(); + + this.logger.log('开始验证用户凭据', { + operation: 'validateUserCredentials', + email: request.email, + hasApiKey: !!request.apiKey, + timestamp: new Date().toISOString(), + }); + + try { + // 1. 验证请求参数 + if (!request.email || !this.isValidEmail(request.email)) { + return { + success: false, + error: '邮箱格式无效', + }; + } + + if (!request.apiKey) { + return { + success: false, + error: 'API Key不能为空', + }; + } + + // 2. 使用用户的API Key测试连接 + const isValid = await this.testUserApiKey(request.email, request.apiKey); + + // 3. 如果API Key有效,获取用户ID + let userId = undefined; + if (isValid) { + const userInfo = await this.getUserInfo({ email: request.email }); + if (userInfo.success) { + userId = userInfo.userId; + } + } + + const duration = Date.now() - startTime; + + this.logger.log('用户凭据验证完成', { + operation: 'validateUserCredentials', + email: request.email, + isValid, + userId, + duration, + timestamp: new Date().toISOString(), + }); + + return { + success: true, + isValid, + userId, + }; + + } catch (error) { + const err = error as Error; + const duration = Date.now() - startTime; + + this.logger.error('验证用户凭据失败', { + operation: 'validateUserCredentials', + email: request.email, + error: err.message, + duration, + timestamp: new Date().toISOString(), + }, err.stack); + + return { + success: false, + error: '系统错误,请稍后重试', + }; + } + } + + /** + * 获取所有用户列表 + * + * 功能描述: + * 从Zulip服务器获取所有用户的列表 + * + * @returns Promise + */ + async getAllUsers(): Promise { + this.logger.debug('开始获取用户列表', { + operation: 'getAllUsers', + timestamp: new Date().toISOString(), + }); + + try { + // 获取Zulip配置 + const config = this.configService.getZulipConfig(); + + // 构建API URL + const apiUrl = `${config.zulipServerUrl}/api/v1/users`; + + // 构建认证头 + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + // 发送请求 + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + this.logger.warn('获取用户列表失败', { + operation: 'getAllUsers', + status: response.status, + statusText: response.statusText, + }); + + return { + success: false, + error: `API调用失败: ${response.status} ${response.statusText}`, + }; + } + + const data: ZulipUsersResponse = await response.json(); + + // 转换数据格式 + const users = data.members?.map(user => ({ + userId: user.user_id, + email: user.email, + fullName: user.full_name, + isActive: user.is_active, + isAdmin: user.is_admin, + isBot: user.is_bot, + })) || []; + + this.logger.debug('用户列表获取完成', { + operation: 'getAllUsers', + userCount: users.length, + timestamp: new Date().toISOString(), + }); + + return { + success: true, + users, + totalCount: users.length, + }; + + } catch (error) { + const err = error as Error; + this.logger.error('获取用户列表异常', { + operation: 'getAllUsers', + error: err.message, + timestamp: new Date().toISOString(), + }, err.stack); + + return { + success: false, + error: '系统错误,请稍后重试', + }; + } + } + + /** + * 测试用户API Key是否有效 + * + * 功能描述: + * 使用用户的API Key测试是否能够成功调用Zulip API + * + * @param email 用户邮箱 + * @param apiKey 用户API Key + * @returns Promise 是否有效 + * @private + */ + private async testUserApiKey(email: string, apiKey: string): Promise { + this.logger.debug('测试用户API Key', { + operation: 'testUserApiKey', + email, + }); + + try { + // 获取Zulip配置 + const config = this.configService.getZulipConfig(); + + // 构建API URL - 使用获取用户自己信息的接口 + const apiUrl = `${config.zulipServerUrl}/api/v1/users/me`; + + // 使用用户的API Key构建认证头 + const auth = Buffer.from(`${email}:${apiKey}`).toString('base64'); + + // 发送请求 + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + const isValid = response.ok; + + this.logger.debug('API Key测试完成', { + operation: 'testUserApiKey', + email, + isValid, + status: response.status, + }); + + return isValid; + + } catch (error) { + const err = error as Error; + this.logger.error('测试API Key异常', { + operation: 'testUserApiKey', + email, + error: err.message, + }); + + return false; + } + } + + /** + * 验证邮箱格式 + * + * @param email 邮箱地址 + * @returns boolean 是否有效 + * @private + */ + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } +} \ No newline at end of file diff --git a/src/core/zulip/services/user_registration.service.spec.ts b/src/core/zulip/services/user_registration.service.spec.ts new file mode 100644 index 0000000..8e7f7ac --- /dev/null +++ b/src/core/zulip/services/user_registration.service.spec.ts @@ -0,0 +1,188 @@ +/** + * Zulip用户注册服务测试 + * + * 功能描述: + * - 测试UserRegistrationService的核心功能 + * - 测试用户注册流程和验证逻辑 + * - 测试错误处理和边界情况 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-06 + */ + +import { Test, TestingModule } from '@nestjs/testing'; +import { UserRegistrationService, UserRegistrationRequest } from './user_registration.service'; +import { IZulipConfigService } from '../interfaces/zulip-core.interfaces'; + +describe('UserRegistrationService', () => { + let service: UserRegistrationService; + let mockConfigService: jest.Mocked; + + beforeEach(async () => { + // 创建模拟的配置服务 + mockConfigService = { + getZulipConfig: jest.fn().mockReturnValue({ + zulipServerUrl: 'https://test.zulip.com', + zulipBotEmail: 'bot@test.com', + zulipBotApiKey: 'test-api-key', + }), + getMapIdByStream: jest.fn(), + getStreamByMap: jest.fn(), + getMapConfig: jest.fn(), + hasMap: jest.fn(), + getAllMapIds: jest.fn(), + getMapConfigByStream: jest.fn(), + getAllStreams: jest.fn(), + hasStream: jest.fn(), + findObjectByTopic: jest.fn(), + getObjectsInMap: jest.fn(), + getTopicByObject: jest.fn(), + findNearbyObject: jest.fn(), + reloadConfig: jest.fn(), + validateConfig: jest.fn(), + getAllMapConfigs: jest.fn(), + getConfigStats: jest.fn(), + getConfigFilePath: jest.fn(), + configFileExists: jest.fn(), + enableConfigWatcher: jest.fn(), + disableConfigWatcher: jest.fn(), + isConfigWatcherEnabled: jest.fn(), + getFullConfiguration: jest.fn(), + updateConfigValue: jest.fn(), + exportMapConfig: jest.fn(), + } as jest.Mocked; + + const module: TestingModule = await Test.createTestingModule({ + providers: [ + UserRegistrationService, + { + provide: 'ZULIP_CONFIG_SERVICE', + useValue: mockConfigService, + }, + ], + }).compile(); + + service = module.get(UserRegistrationService); + }); + + it('应该正确初始化服务', () => { + expect(service).toBeDefined(); + }); + + describe('registerUser - 用户注册', () => { + it('应该成功注册有效用户', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: 'Test User', + password: 'password123', + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(true); + expect(result.email).toBe(request.email); + expect(result.userId).toBeDefined(); + expect(result.apiKey).toBeDefined(); + expect(result.error).toBeUndefined(); + }); + + it('应该拒绝无效邮箱', async () => { + const request: UserRegistrationRequest = { + email: 'invalid-email', + fullName: 'Test User', + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('邮箱格式无效'); + }); + + it('应该拒绝空邮箱', async () => { + const request: UserRegistrationRequest = { + email: '', + fullName: 'Test User', + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('邮箱不能为空'); + }); + + it('应该拒绝空用户名', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: '', + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('用户全名不能为空'); + }); + + it('应该拒绝过短的用户名', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: 'A', + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('用户全名至少需要2个字符'); + }); + + it('应该拒绝过长的用户名', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: 'A'.repeat(101), // 101个字符 + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('用户全名不能超过100个字符'); + }); + + it('应该拒绝过短的密码', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: 'Test User', + password: '123', // 只有3个字符 + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('密码至少需要6个字符'); + }); + + it('应该接受没有密码的注册', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: 'Test User', + // 不提供密码 + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(true); + }); + + it('应该拒绝过长的短名称', async () => { + const request: UserRegistrationRequest = { + email: 'test@example.com', + fullName: 'Test User', + shortName: 'A'.repeat(51), // 51个字符 + }; + + const result = await service.registerUser(request); + + expect(result.success).toBe(false); + expect(result.error).toContain('短名称不能超过50个字符'); + }); + }); +}); \ No newline at end of file diff --git a/src/core/zulip/services/user_registration.service.ts b/src/core/zulip/services/user_registration.service.ts new file mode 100644 index 0000000..5040cb6 --- /dev/null +++ b/src/core/zulip/services/user_registration.service.ts @@ -0,0 +1,531 @@ +/** + * Zulip用户管理服务 + * + * 功能描述: + * - 查询和验证Zulip用户信息 + * - 检查用户是否存在 + * - 获取用户详细信息 + * - 管理用户API Key(如果有权限) + * + * 主要方法: + * - checkUserExists(): 检查用户是否存在 + * - getUserInfo(): 获取用户详细信息 + * - validateUserCredentials(): 验证用户凭据 + * - getUserApiKey(): 获取用户API Key(需要管理员权限) + * + * 使用场景: + * - 用户登录时验证用户存在性 + * - 获取用户基本信息 + * - 验证用户权限和状态 + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-06 + */ + +import { Injectable, Logger, Inject } from '@nestjs/common'; +import { IZulipConfigService } from '../interfaces/zulip-core.interfaces'; + +/** + * Zulip API响应接口 + */ +interface ZulipApiResponse { + result?: 'success' | 'error'; + msg?: string; + message?: string; +} + +/** + * 用户列表响应接口 + */ +interface ZulipUsersResponse extends ZulipApiResponse { + members?: Array<{ + email: string; + user_id: number; + full_name: string; + }>; +} + +/** + * 创建用户响应接口 + */ +interface ZulipCreateUserResponse extends ZulipApiResponse { + user_id?: number; +} + +/** + * API Key响应接口 + */ +interface ZulipApiKeyResponse extends ZulipApiResponse { + api_key?: string; +} +export interface UserRegistrationRequest { + email: string; + fullName: string; + password?: string; + shortName?: string; +} + +/** + * 用户注册响应接口 + */ +export interface UserRegistrationResponse { + success: boolean; + userId?: number; + email?: string; + apiKey?: string; + error?: string; + details?: any; +} + +/** + * Zulip用户注册服务类 + * + * 职责: + * - 处理新用户在Zulip服务器上的注册 + * - 验证用户信息的有效性 + * - 与Zulip API交互创建用户账户 + * - 管理注册流程和错误处理 + */ +@Injectable() +export class UserRegistrationService { + private readonly logger = new Logger(UserRegistrationService.name); + + constructor( + @Inject('ZULIP_CONFIG_SERVICE') + private readonly configService: IZulipConfigService, + ) { + this.logger.log('UserRegistrationService初始化完成'); + } + + /** + * 注册新用户到Zulip服务器 + * + * 功能描述: + * 在Zulip服务器上创建新用户账户 + * + * 业务逻辑: + * 1. 验证用户注册信息 + * 2. 检查用户是否已存在 + * 3. 调用Zulip API创建用户 + * 4. 获取用户API Key + * 5. 返回注册结果 + * + * @param request 用户注册请求数据 + * @returns Promise + */ + async registerUser(request: UserRegistrationRequest): Promise { + const startTime = Date.now(); + + this.logger.log('开始注册Zulip用户', { + operation: 'registerUser', + email: request.email, + fullName: request.fullName, + timestamp: new Date().toISOString(), + }); + + try { + // 1. 验证用户注册信息 + const validationResult = this.validateUserInfo(request); + if (!validationResult.valid) { + this.logger.warn('用户注册信息验证失败', { + operation: 'registerUser', + email: request.email, + errors: validationResult.errors, + }); + + return { + success: false, + error: validationResult.errors.join(', '), + }; + } + + // TODO: 实现实际的Zulip用户注册逻辑 + // 这里先返回模拟结果,后续步骤中实现真实的API调用 + + // 2. 检查用户是否已存在 + const userExists = await this.checkUserExists(request.email); + if (userExists) { + this.logger.warn('用户注册失败:用户已存在', { + operation: 'registerUser', + email: request.email, + }); + + return { + success: false, + error: '用户已存在', + }; + } + + // 3. 调用Zulip API创建用户 + const createResult = await this.createZulipUser(request); + if (!createResult.success) { + return { + success: false, + error: createResult.error || '创建用户失败', + }; + } + + // 4. 获取用户API Key(如果需要) + let apiKey = undefined; + if (createResult.userId) { + const apiKeyResult = await this.generateApiKey(createResult.userId, request.email); + if (apiKeyResult.success) { + apiKey = apiKeyResult.apiKey; + } + } + + const duration = Date.now() - startTime; + + this.logger.log('Zulip用户注册完成(模拟)', { + operation: 'registerUser', + email: request.email, + duration, + timestamp: new Date().toISOString(), + }); + + return { + success: true, + userId: createResult.userId, + email: request.email, + apiKey: apiKey, + }; + + } catch (error) { + const err = error as Error; + const duration = Date.now() - startTime; + + this.logger.error('Zulip用户注册失败', { + operation: 'registerUser', + email: request.email, + error: err.message, + duration, + timestamp: new Date().toISOString(), + }, err.stack); + + return { + success: false, + error: '注册失败,请稍后重试', + }; + } + } + + /** + * 验证用户注册信息 + * + * 功能描述: + * 验证用户提供的注册信息是否有效 + * + * @param request 用户注册请求 + * @returns {valid: boolean, errors: string[]} 验证结果 + * @private + */ + private validateUserInfo(request: UserRegistrationRequest): { + valid: boolean; + errors: string[]; + } { + const errors: string[] = []; + + // 验证邮箱 + if (!request.email || !request.email.trim()) { + errors.push('邮箱不能为空'); + } else if (!this.isValidEmail(request.email)) { + errors.push('邮箱格式无效'); + } + + // 验证全名 + if (!request.fullName || !request.fullName.trim()) { + errors.push('用户全名不能为空'); + } else if (request.fullName.trim().length < 2) { + errors.push('用户全名至少需要2个字符'); + } else if (request.fullName.trim().length > 100) { + errors.push('用户全名不能超过100个字符'); + } + + // 验证密码(如果提供) + if (request.password && request.password.length < 6) { + errors.push('密码至少需要6个字符'); + } + + // 验证短名称(如果提供) + if (request.shortName && request.shortName.trim().length > 50) { + errors.push('短名称不能超过50个字符'); + } + + return { + valid: errors.length === 0, + errors, + }; + } + + /** + * 验证邮箱格式 + * + * @param email 邮箱地址 + * @returns boolean 是否有效 + * @private + */ + private isValidEmail(email: string): boolean { + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/; + return emailRegex.test(email); + } + + /** + * 检查用户是否已存在 + * + * 功能描述: + * 通过Zulip API检查指定邮箱的用户是否已存在 + * + * @param email 用户邮箱 + * @returns Promise 是否存在 + * @private + */ + private async checkUserExists(email: string): Promise { + this.logger.debug('检查用户是否存在', { + operation: 'checkUserExists', + email, + }); + + try { + // 获取Zulip配置 + const config = this.configService.getZulipConfig(); + + // 构建API URL + const apiUrl = `${config.zulipServerUrl}/api/v1/users`; + + // 构建认证头 + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + // 发送请求 + const response = await fetch(apiUrl, { + method: 'GET', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + this.logger.warn('获取用户列表失败', { + operation: 'checkUserExists', + status: response.status, + statusText: response.statusText, + }); + return false; // 如果API调用失败,假设用户不存在 + } + + const data: ZulipUsersResponse = await response.json(); + + // 检查用户是否在列表中 + if (data.members && Array.isArray(data.members)) { + const userExists = data.members.some((user: any) => + user.email && user.email.toLowerCase() === email.toLowerCase() + ); + + this.logger.debug('用户存在性检查完成', { + operation: 'checkUserExists', + email, + exists: userExists, + }); + + return userExists; + } + + return false; + + } catch (error) { + const err = error as Error; + this.logger.error('检查用户存在性失败', { + operation: 'checkUserExists', + email, + error: err.message, + }); + + // 如果检查失败,假设用户不存在,允许继续注册 + return false; + } + } + + /** + * 创建Zulip用户 + * + * 功能描述: + * 通过Zulip API创建新用户账户 + * + * @param request 用户注册请求 + * @returns Promise<{success: boolean, userId?: number, error?: string}> + * @private + */ + private async createZulipUser(request: UserRegistrationRequest): Promise<{ + success: boolean; + userId?: number; + error?: string; + }> { + this.logger.log('开始创建Zulip用户', { + operation: 'createZulipUser', + email: request.email, + fullName: request.fullName, + }); + + try { + // 获取Zulip配置 + const config = this.configService.getZulipConfig(); + + // 构建API URL + const apiUrl = `${config.zulipServerUrl}/api/v1/users`; + + // 构建认证头 + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + // 构建请求体 + const requestBody = new URLSearchParams(); + requestBody.append('email', request.email); + requestBody.append('full_name', request.fullName); + + if (request.password) { + requestBody.append('password', request.password); + } + + if (request.shortName) { + requestBody.append('short_name', request.shortName); + } + + // 发送请求 + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: requestBody.toString(), + }); + + const data: ZulipCreateUserResponse = await response.json(); + + if (!response.ok) { + this.logger.warn('Zulip用户创建失败', { + operation: 'createZulipUser', + email: request.email, + status: response.status, + statusText: response.statusText, + error: data.msg || data.message, + }); + + return { + success: false, + error: data.msg || data.message || '创建用户失败', + }; + } + + this.logger.log('Zulip用户创建成功', { + operation: 'createZulipUser', + email: request.email, + userId: data.user_id, + }); + + return { + success: true, + userId: data.user_id, + }; + + } catch (error) { + const err = error as Error; + this.logger.error('创建Zulip用户异常', { + operation: 'createZulipUser', + email: request.email, + error: err.message, + }, err.stack); + + return { + success: false, + error: '系统错误,请稍后重试', + }; + } + } + + /** + * 为用户生成API Key + * + * 功能描述: + * 为新创建的用户生成API Key,用于后续的Zulip API调用 + * + * @param userId 用户ID + * @param email 用户邮箱 + * @returns Promise<{success: boolean, apiKey?: string, error?: string}> + * @private + */ + private async generateApiKey(userId: number, email: string): Promise<{ + success: boolean; + apiKey?: string; + error?: string; + }> { + this.logger.log('开始生成用户API Key', { + operation: 'generateApiKey', + userId, + email, + }); + + try { + // 获取Zulip配置 + const config = this.configService.getZulipConfig(); + + // 构建API URL + const apiUrl = `${config.zulipServerUrl}/api/v1/users/${userId}/api_key/regenerate`; + + // 构建认证头 + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + // 发送请求 + const response = await fetch(apiUrl, { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + const data: ZulipApiKeyResponse = await response.json(); + + if (!response.ok) { + this.logger.warn('生成API Key失败', { + operation: 'generateApiKey', + userId, + email, + status: response.status, + statusText: response.statusText, + error: data.msg || data.message, + }); + + return { + success: false, + error: data.msg || data.message || '生成API Key失败', + }; + } + + this.logger.log('API Key生成成功', { + operation: 'generateApiKey', + userId, + email, + }); + + return { + success: true, + apiKey: data.api_key, + }; + + } catch (error) { + const err = error as Error; + this.logger.error('生成API Key异常', { + operation: 'generateApiKey', + userId, + email, + error: err.message, + }, err.stack); + + return { + success: false, + error: '系统错误,请稍后重试', + }; + } + } +} \ No newline at end of file diff --git a/test_zulip_registration.js b/test_zulip_registration.js new file mode 100644 index 0000000..132b6a0 --- /dev/null +++ b/test_zulip_registration.js @@ -0,0 +1,196 @@ +/** + * Zulip用户注册真实环境测试脚本 + * + * 功能描述: + * - 测试Zulip用户注册功能在真实环境下的表现 + * - 验证API调用是否正常工作 + * - 检查配置是否正确 + * + * 使用方法: + * node test_zulip_registration.js + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-06 + */ + +const https = require('https'); +const { URLSearchParams } = require('url'); + +// 配置信息 +const config = { + zulipServerUrl: 'https://zulip.xinghangee.icu', + zulipBotEmail: 'angjustinl@mail.angforever.top', + zulipBotApiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8', +}; + +/** + * 检查用户是否存在 + */ +async function checkUserExists(email) { + console.log(`🔍 检查用户是否存在: ${email}`); + + try { + const url = `${config.zulipServerUrl}/api/v1/users`; + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + console.log(`❌ 获取用户列表失败: ${response.status} ${response.statusText}`); + return false; + } + + const data = await response.json(); + console.log(`📊 获取到 ${data.members?.length || 0} 个用户`); + + if (data.members && Array.isArray(data.members)) { + const userExists = data.members.some(user => + user.email && user.email.toLowerCase() === email.toLowerCase() + ); + + console.log(`✅ 用户存在性检查完成: ${userExists ? '存在' : '不存在'}`); + return userExists; + } + + return false; + + } catch (error) { + console.error(`❌ 检查用户存在性失败:`, error.message); + return false; + } +} + +/** + * 创建测试用户 + */ +async function createTestUser(email, fullName, password) { + console.log(`🚀 开始创建用户: ${email}`); + + try { + const url = `${config.zulipServerUrl}/api/v1/users`; + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + const requestBody = new URLSearchParams(); + requestBody.append('email', email); + requestBody.append('full_name', fullName); + + if (password) { + requestBody.append('password', password); + } + + const response = await fetch(url, { + method: 'POST', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/x-www-form-urlencoded', + }, + body: requestBody.toString(), + }); + + const data = await response.json(); + + if (!response.ok) { + console.log(`❌ 用户创建失败: ${response.status} ${response.statusText}`); + console.log(`📝 错误信息: ${data.msg || data.message || '未知错误'}`); + return { success: false, error: data.msg || data.message }; + } + + console.log(`✅ 用户创建成功! 用户ID: ${data.user_id}`); + return { success: true, userId: data.user_id }; + + } catch (error) { + console.error(`❌ 创建用户异常:`, error.message); + return { success: false, error: error.message }; + } +} + +/** + * 测试连接 + */ +async function testConnection() { + console.log('🔗 测试Zulip服务器连接...'); + + try { + const url = `${config.zulipServerUrl}/api/v1/server_settings`; + const response = await fetch(url); + + if (response.ok) { + const data = await response.json(); + console.log(`✅ 连接成功! 服务器版本: ${data.zulip_version || '未知'}`); + return true; + } else { + console.log(`❌ 连接失败: ${response.status} ${response.statusText}`); + return false; + } + } catch (error) { + console.error(`❌ 连接异常:`, error.message); + return false; + } +} + +/** + * 主测试函数 + */ +async function main() { + console.log('🎯 开始Zulip用户注册测试'); + console.log('=' * 50); + + // 1. 测试连接 + const connected = await testConnection(); + if (!connected) { + console.log('❌ 无法连接到Zulip服务器,测试终止'); + return; + } + + console.log(''); + + // 2. 生成测试用户信息 + const timestamp = Date.now(); + const testEmail = `test_user_${timestamp}@example.com`; + const testFullName = `Test User ${timestamp}`; + const testPassword = 'test123456'; + + console.log(`📋 测试用户信息:`); + console.log(` 邮箱: ${testEmail}`); + console.log(` 姓名: ${testFullName}`); + console.log(` 密码: ${testPassword}`); + console.log(''); + + // 3. 检查用户是否已存在 + const userExists = await checkUserExists(testEmail); + if (userExists) { + console.log('⚠️ 用户已存在,跳过创建测试'); + return; + } + + console.log(''); + + // 4. 创建用户 + const createResult = await createTestUser(testEmail, testFullName, testPassword); + + console.log(''); + console.log('📊 测试结果:'); + if (createResult.success) { + console.log('✅ 用户注册功能正常工作'); + console.log(` 新用户ID: ${createResult.userId}`); + } else { + console.log('❌ 用户注册功能存在问题'); + console.log(` 错误信息: ${createResult.error}`); + } + + console.log(''); + console.log('🎉 测试完成'); +} + +// 运行测试 +main().catch(error => { + console.error('💥 测试过程中发生未处理的错误:', error); + process.exit(1); +}); \ No newline at end of file diff --git a/test_zulip_user_management.js b/test_zulip_user_management.js new file mode 100644 index 0000000..dd14baf --- /dev/null +++ b/test_zulip_user_management.js @@ -0,0 +1,275 @@ +/** + * Zulip用户管理真实环境测试脚本 + * + * 功能描述: + * - 测试Zulip用户管理功能在真实环境下的表现 + * - 验证用户查询、验证等API调用是否正常工作 + * - 检查配置是否正确 + * + * 使用方法: + * node test_zulip_user_management.js + * + * @author angjustinl + * @version 1.0.0 + * @since 2025-01-06 + */ + +const https = require('https'); + +// 配置信息 +const config = { + zulipServerUrl: 'https://zulip.xinghangee.icu', + zulipBotEmail: 'angjustinl@mail.angforever.top', + zulipBotApiKey: 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8', +}; + +/** + * 获取所有用户列表 + */ +async function getAllUsers() { + console.log('📋 获取所有用户列表...'); + + try { + const url = `${config.zulipServerUrl}/api/v1/users`; + const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64'); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + if (!response.ok) { + console.log(`❌ 获取用户列表失败: ${response.status} ${response.statusText}`); + return { success: false, error: `${response.status} ${response.statusText}` }; + } + + const data = await response.json(); + const users = data.members?.map(user => ({ + userId: user.user_id, + email: user.email, + fullName: user.full_name, + isActive: user.is_active, + isAdmin: user.is_admin, + isBot: user.is_bot, + })) || []; + + console.log(`✅ 成功获取 ${users.length} 个用户`); + + // 显示前几个用户信息 + console.log('👥 用户列表预览:'); + users.slice(0, 5).forEach((user, index) => { + console.log(` ${index + 1}. ${user.fullName} (${user.email})`); + console.log(` ID: ${user.userId}, 活跃: ${user.isActive}, 管理员: ${user.isAdmin}, 机器人: ${user.isBot}`); + }); + + if (users.length > 5) { + console.log(` ... 还有 ${users.length - 5} 个用户`); + } + + return { success: true, users, totalCount: users.length }; + + } catch (error) { + console.error(`❌ 获取用户列表异常:`, error.message); + return { success: false, error: error.message }; + } +} + +/** + * 检查指定用户是否存在 + */ +async function checkUserExists(email) { + console.log(`🔍 检查用户是否存在: ${email}`); + + try { + const usersResult = await getAllUsers(); + if (!usersResult.success) { + console.log(`❌ 无法获取用户列表: ${usersResult.error}`); + return false; + } + + const userExists = usersResult.users.some(user => + user.email.toLowerCase() === email.toLowerCase() + ); + + console.log(`✅ 用户存在性检查完成: ${userExists ? '存在' : '不存在'}`); + return userExists; + + } catch (error) { + console.error(`❌ 检查用户存在性失败:`, error.message); + return false; + } +} + +/** + * 获取用户详细信息 + */ +async function getUserInfo(email) { + console.log(`📝 获取用户信息: ${email}`); + + try { + const usersResult = await getAllUsers(); + if (!usersResult.success) { + console.log(`❌ 无法获取用户列表: ${usersResult.error}`); + return { success: false, error: usersResult.error }; + } + + const user = usersResult.users.find(u => + u.email.toLowerCase() === email.toLowerCase() + ); + + if (!user) { + console.log(`❌ 用户不存在: ${email}`); + return { success: false, error: '用户不存在' }; + } + + console.log(`✅ 用户信息获取成功:`); + console.log(` 用户ID: ${user.userId}`); + console.log(` 邮箱: ${user.email}`); + console.log(` 姓名: ${user.fullName}`); + console.log(` 状态: ${user.isActive ? '活跃' : '非活跃'}`); + console.log(` 权限: ${user.isAdmin ? '管理员' : '普通用户'}`); + console.log(` 类型: ${user.isBot ? '机器人' : '真实用户'}`); + + return { success: true, user }; + + } catch (error) { + console.error(`❌ 获取用户信息失败:`, error.message); + return { success: false, error: error.message }; + } +} + +/** + * 测试用户API Key + */ +async function testUserApiKey(email, apiKey) { + console.log(`🔑 测试用户API Key: ${email}`); + + try { + const url = `${config.zulipServerUrl}/api/v1/users/me`; + const auth = Buffer.from(`${email}:${apiKey}`).toString('base64'); + + const response = await fetch(url, { + method: 'GET', + headers: { + 'Authorization': `Basic ${auth}`, + 'Content-Type': 'application/json', + }, + }); + + const isValid = response.ok; + + if (isValid) { + const data = await response.json(); + console.log(`✅ API Key有效! 用户信息:`); + console.log(` 用户ID: ${data.user_id}`); + console.log(` 邮箱: ${data.email}`); + console.log(` 姓名: ${data.full_name}`); + } else { + console.log(`❌ API Key无效: ${response.status} ${response.statusText}`); + } + + return isValid; + + } catch (error) { + console.error(`❌ 测试API Key异常:`, error.message); + return false; + } +} + +/** + * 测试连接 + */ +async function testConnection() { + console.log('🔗 测试Zulip服务器连接...'); + + try { + const url = `${config.zulipServerUrl}/api/v1/server_settings`; + const response = await fetch(url); + + if (response.ok) { + const data = await response.json(); + console.log(`✅ 连接成功! 服务器信息:`); + console.log(` 版本: ${data.zulip_version || '未知'}`); + console.log(` 服务器: ${data.realm_name || '未知'}`); + return true; + } else { + console.log(`❌ 连接失败: ${response.status} ${response.statusText}`); + return false; + } + } catch (error) { + console.error(`❌ 连接异常:`, error.message); + return false; + } +} + +/** + * 主测试函数 + */ +async function main() { + console.log('🎯 开始Zulip用户管理测试'); + console.log('='.repeat(50)); + + // 1. 测试连接 + const connected = await testConnection(); + if (!connected) { + console.log('❌ 无法连接到Zulip服务器,测试终止'); + return; + } + + console.log(''); + + // 2. 获取所有用户列表 + const usersResult = await getAllUsers(); + if (!usersResult.success) { + console.log('❌ 无法获取用户列表,测试终止'); + return; + } + + console.log(''); + + // 3. 测试用户存在性检查 + const testEmails = [ + 'angjustinl@mail.angforever.top', // 应该存在 + 'nonexistent@example.com', // 应该不存在 + ]; + + console.log('🔍 测试用户存在性检查:'); + for (const email of testEmails) { + const exists = await checkUserExists(email); + console.log(` ${email}: ${exists ? '✅ 存在' : '❌ 不存在'}`); + } + + console.log(''); + + // 4. 测试获取用户信息 + console.log('📝 测试获取用户信息:'); + const existingEmail = 'angjustinl@mail.angforever.top'; + const userInfoResult = await getUserInfo(existingEmail); + + console.log(''); + + // 5. 测试API Key验证(如果有的话) + console.log('🔑 测试API Key验证:'); + const testApiKey = 'lCPWCPfGh7WUHxwN56GF8oYXOpqNfGF8'; // 这是我们的测试API Key + const apiKeyValid = await testUserApiKey(existingEmail, testApiKey); + + console.log(''); + console.log('📊 测试结果总结:'); + console.log(`✅ 服务器连接: 正常`); + console.log(`✅ 用户列表获取: 正常 (${usersResult.totalCount} 个用户)`); + console.log(`✅ 用户存在性检查: 正常`); + console.log(`✅ 用户信息获取: ${userInfoResult.success ? '正常' : '异常'}`); + console.log(`✅ API Key验证: ${apiKeyValid ? '正常' : '异常'}`); + + console.log(''); + console.log('🎉 用户管理功能测试完成'); +} + +// 运行测试 +main().catch(error => { + console.error('💥 测试过程中发生未处理的错误:', error); + process.exit(1); +}); \ No newline at end of file