/** * 登录业务服务 * * 功能描述: * - 处理用户登录相关的业务逻辑和流程控制 * - 整合核心服务,提供完整的登录功能 * - 处理业务规则、数据格式化和错误处理 * - 管理JWT令牌刷新和验证码登录 * * 职责分离: * - 专注于登录业务流程和规则实现 * - 调用核心服务完成具体功能 * - 为控制器层提供登录业务接口 * - JWT技术实现已移至Core层,符合架构分层原则 * * 最近修改: * - 2026-01-12: 代码分离 - 移除注册相关业务逻辑,专注于登录功能 * - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构 * - 2026-01-07: 架构优化 - 将JWT技术实现移至login_core模块,符合架构分层原则 * * @author moyin * @version 1.1.0 * @since 2025-12-17 * @lastModified 2026-01-12 */ import { Injectable, Logger, Inject } from '@nestjs/common'; import { LoginCoreService, LoginRequest, GitHubOAuthRequest, PasswordResetRequest, VerificationCodeLoginRequest, TokenPair } from '../../core/login_core/login_core.service'; import { Users } from '../../core/db/users/users.entity'; import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service'; import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service'; import { AccountProfilePayload, AccountProfileService } from './account_profile.service'; // Import the interface types we need interface IZulipAccountsService { findByGameUserId(gameUserId: string, includeGameUser?: boolean): Promise; create(createDto: any): Promise; deleteByGameUserId(gameUserId: string): Promise; } // 常量定义 const ERROR_CODES = { LOGIN_FAILED: 'LOGIN_FAILED', GITHUB_OAUTH_FAILED: 'GITHUB_OAUTH_FAILED', SEND_CODE_FAILED: 'SEND_CODE_FAILED', RESET_PASSWORD_FAILED: 'RESET_PASSWORD_FAILED', CHANGE_PASSWORD_FAILED: 'CHANGE_PASSWORD_FAILED', VERIFICATION_CODE_LOGIN_FAILED: 'VERIFICATION_CODE_LOGIN_FAILED', SEND_LOGIN_CODE_FAILED: 'SEND_LOGIN_CODE_FAILED', TOKEN_REFRESH_FAILED: 'TOKEN_REFRESH_FAILED', DEBUG_VERIFICATION_CODE_FAILED: 'DEBUG_VERIFICATION_CODE_FAILED', TEST_MODE_ONLY: 'TEST_MODE_ONLY', INVALID_VERIFICATION_CODE: 'INVALID_VERIFICATION_CODE', } as const; const MESSAGES = { LOGIN_SUCCESS: '登录成功', GITHUB_LOGIN_SUCCESS: 'GitHub登录成功', GITHUB_BIND_SUCCESS: 'GitHub账户绑定成功', PASSWORD_RESET_SUCCESS: '密码重置成功', PASSWORD_CHANGE_SUCCESS: '密码修改成功', VERIFICATION_CODE_LOGIN_SUCCESS: '验证码登录成功', TOKEN_REFRESH_SUCCESS: '令牌刷新成功', DEBUG_INFO_SUCCESS: '调试信息获取成功', CODE_SENT: '验证码已发送,请查收', VERIFICATION_CODE_ERROR: '验证码错误', TEST_MODE_WARNING: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。', } as const; // JWT相关接口已移至Core层,通过import导入 /** * 登录响应数据接口 */ export interface LoginResponse { /** 用户信息 */ user: { id: string; username: string; nickname: string; email?: string; phone?: string; avatar_url?: string; role: number; created_at: Date; }; /** 游戏内账号资料 */ profile?: AccountProfilePayload['profile']; /** 访问令牌 */ access_token: string; /** 刷新令牌 */ refresh_token: string; /** 访问令牌过期时间(秒) */ expires_in: number; /** 令牌类型 */ token_type: string; /** 是否为新用户 */ is_new_user?: boolean; /** 消息 */ message: string; } /** * 通用响应接口 */ export interface ApiResponse { /** 是否成功 */ success: boolean; /** 响应数据 */ data?: T; /** 消息 */ message: string; /** 错误代码 */ error_code?: string; } @Injectable() export class LoginService { private readonly logger = new Logger(LoginService.name); constructor( private readonly loginCoreService: LoginCoreService, private readonly zulipAccountService: ZulipAccountService, @Inject('ZulipAccountsService') private readonly zulipAccountsService: IZulipAccountsService, private readonly apiKeySecurityService: ApiKeySecurityService, private readonly accountProfileService: AccountProfileService, ) {} /** * 用户登录 * * 功能描述: * 处理用户登录请求,验证用户凭据并生成JWT令牌 * * 业务逻辑: * 1. 调用核心服务进行用户认证 * 2. 生成JWT访问令牌和刷新令牌 * 3. 记录登录日志和安全审计 * 4. 返回用户信息和令牌 * * @param loginRequest 登录请求数据 * @returns Promise> 登录响应 * * @throws BadRequestException 当登录参数无效时 * @throws UnauthorizedException 当用户凭据错误时 * @throws InternalServerErrorException 当系统错误时 */ async login(loginRequest: LoginRequest): Promise> { const startTime = Date.now(); try { this.logger.log('用户登录尝试', { operation: 'login', identifier: loginRequest.identifier, timestamp: new Date().toISOString(), }); // 1. 调用核心服务进行认证 const authResult = await this.loginCoreService.login(loginRequest); // 2. 验证和更新Zulip API Key(如果用户有Zulip账号关联) try { const isZulipValid = await this.validateAndUpdateZulipApiKey(authResult.user); if (!isZulipValid) { // 尝试重新生成API Key(需要密码) const regenerated = await this.regenerateZulipApiKey(authResult.user, loginRequest.password); if (regenerated) { this.logger.log('用户Zulip API Key已重新生成', { operation: 'login', userId: authResult.user.id.toString(), }); } else { this.logger.warn('用户Zulip API Key重新生成失败', { operation: 'login', userId: authResult.user.id.toString(), }); } } } catch (zulipError) { // Zulip验证失败不影响登录流程,只记录日志 const err = zulipError as Error; this.logger.warn('Zulip API Key验证失败,但不影响登录', { operation: 'login', userId: authResult.user.id.toString(), zulipError: err.message, }); } // 3. 生成JWT令牌对(通过Core层) const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user); const profile = await this.accountProfileService.ensureProfile(authResult.user.id); // 4. 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), profile: (await this.accountProfileService.formatAccountProfileAsync(authResult.user, profile)).profile, access_token: tokenPair.access_token, refresh_token: tokenPair.refresh_token, expires_in: tokenPair.expires_in, token_type: tokenPair.token_type, is_new_user: authResult.isNewUser, message: MESSAGES.LOGIN_SUCCESS }; const duration = Date.now() - startTime; this.logger.log('用户登录成功', { operation: 'login', userId: authResult.user.id.toString(), username: authResult.user.username, isNewUser: authResult.isNewUser, duration, timestamp: new Date().toISOString(), }); return { success: true, data: response, message: MESSAGES.LOGIN_SUCCESS }; } catch (error) { const duration = Date.now() - startTime; const err = error as Error; this.logger.error('用户登录失败', { operation: 'login', identifier: loginRequest.identifier, error: err.message, duration, timestamp: new Date().toISOString(), }, err.stack); return { success: false, message: err.message || '登录失败', error_code: ERROR_CODES.LOGIN_FAILED }; } } /** * GitHub OAuth登录 * * @param oauthRequest OAuth请求 * @returns 登录响应 */ async githubOAuth(oauthRequest: GitHubOAuthRequest): Promise> { try { this.logger.log(`GitHub OAuth登录尝试: ${oauthRequest.github_id}`); // 调用核心服务进行OAuth认证 const authResult = await this.loginCoreService.githubOAuth(oauthRequest); // 生成JWT令牌对(通过Core层) const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user); // 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), access_token: tokenPair.access_token, refresh_token: tokenPair.refresh_token, expires_in: tokenPair.expires_in, token_type: tokenPair.token_type, is_new_user: authResult.isNewUser, message: authResult.isNewUser ? MESSAGES.GITHUB_BIND_SUCCESS : MESSAGES.GITHUB_LOGIN_SUCCESS }; this.logger.log(`GitHub OAuth成功: ${authResult.user.username} (ID: ${authResult.user.id})`); return { success: true, data: response, message: response.message }; } catch (error) { this.logger.error(`GitHub OAuth失败: ${oauthRequest.github_id}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : 'GitHub登录失败', error_code: ERROR_CODES.GITHUB_OAUTH_FAILED }; } } /** * 发送密码重置验证码 * * @param identifier 邮箱或手机号 * @returns 响应结果 */ async sendPasswordResetCode(identifier: string): Promise> { try { this.logger.log(`发送密码重置验证码: ${identifier}`); // 调用核心服务发送验证码 const result = await this.loginCoreService.sendPasswordResetCode(identifier); this.logger.log(`密码重置验证码已发送: ${identifier}`); // 处理测试模式响应 if (result.isTestMode) { return { success: false, data: { verification_code: result.code, is_test_mode: true }, message: MESSAGES.TEST_MODE_WARNING, error_code: ERROR_CODES.TEST_MODE_ONLY }; } else { return { success: true, data: { is_test_mode: false }, message: MESSAGES.CODE_SENT }; } } catch (error) { this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '发送验证码失败', error_code: ERROR_CODES.SEND_CODE_FAILED }; } } /** * 重置密码 * * @param resetRequest 重置请求 * @returns 响应结果 */ async resetPassword(resetRequest: PasswordResetRequest): Promise { try { this.logger.log(`密码重置尝试: ${resetRequest.identifier}`); // 调用核心服务重置密码 await this.loginCoreService.resetPassword(resetRequest); this.logger.log(`密码重置成功: ${resetRequest.identifier}`); return { success: true, message: MESSAGES.PASSWORD_RESET_SUCCESS }; } catch (error) { this.logger.error(`密码重置失败: ${resetRequest.identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '密码重置失败', error_code: ERROR_CODES.RESET_PASSWORD_FAILED }; } } /** * 修改密码 * * @param userId 用户ID * @param oldPassword 旧密码 * @param newPassword 新密码 * @returns 响应结果 */ async changePassword(userId: bigint, oldPassword: string, newPassword: string): Promise { try { this.logger.log(`修改密码尝试: 用户ID ${userId}`); // 调用核心服务修改密码 await this.loginCoreService.changePassword(userId, oldPassword, newPassword); this.logger.log(`修改密码成功: 用户ID ${userId}`); return { success: true, message: MESSAGES.PASSWORD_CHANGE_SUCCESS }; } catch (error) { this.logger.error(`修改密码失败: 用户ID ${userId}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '密码修改失败', error_code: ERROR_CODES.CHANGE_PASSWORD_FAILED }; } } /** * 格式化用户信息 * * @param user 用户实体 * @returns 格式化的用户信息 */ private formatUserInfo(user: Users) { return { id: user.id.toString(), // 将bigint转换为字符串 username: user.username, nickname: user.nickname, email: user.email, phone: user.phone, avatar_url: user.avatar_url, role: user.role, created_at: user.created_at }; } /** * 验证码登录 * * @param loginRequest 验证码登录请求 * @returns 登录响应 */ async verificationCodeLogin(loginRequest: VerificationCodeLoginRequest): Promise> { try { this.logger.log(`验证码登录尝试: ${loginRequest.identifier}`); // 调用核心服务进行验证码认证 const authResult = await this.loginCoreService.verificationCodeLogin(loginRequest); // 生成JWT令牌对(通过Core层) const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user); // 格式化响应数据 const response: LoginResponse = { user: this.formatUserInfo(authResult.user), access_token: tokenPair.access_token, refresh_token: tokenPair.refresh_token, expires_in: tokenPair.expires_in, token_type: tokenPair.token_type, is_new_user: authResult.isNewUser, message: MESSAGES.VERIFICATION_CODE_LOGIN_SUCCESS }; this.logger.log(`验证码登录成功: ${authResult.user.username} (ID: ${authResult.user.id})`); return { success: true, data: response, message: MESSAGES.VERIFICATION_CODE_LOGIN_SUCCESS }; } catch (error) { this.logger.error(`验证码登录失败: ${loginRequest.identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '验证码登录失败', error_code: ERROR_CODES.VERIFICATION_CODE_LOGIN_FAILED }; } } /** * 发送登录验证码 * * @param identifier 邮箱或手机号 * @returns 响应结果 */ async sendLoginVerificationCode(identifier: string): Promise> { try { this.logger.log(`发送登录验证码: ${identifier}`); // 调用核心服务发送验证码 const result = await this.loginCoreService.sendLoginVerificationCode(identifier); this.logger.log(`登录验证码已发送: ${identifier}`); // 处理测试模式响应 if (result.isTestMode) { return { success: false, data: { verification_code: result.code, is_test_mode: true }, message: MESSAGES.TEST_MODE_WARNING, error_code: ERROR_CODES.TEST_MODE_ONLY }; } else { return { success: true, data: { is_test_mode: false }, message: MESSAGES.CODE_SENT }; } } catch (error) { this.logger.error(`发送登录验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '发送验证码失败', error_code: ERROR_CODES.SEND_LOGIN_CODE_FAILED }; } } /** * 刷新访问令牌 * * 功能描述: * 使用有效的刷新令牌生成新的访问令牌,实现无感知的令牌续期 * * 业务逻辑: * 1. 验证刷新令牌的有效性和格式 * 2. 检查用户状态是否正常 * 3. 生成新的JWT令牌对 * 4. 返回新的访问令牌和刷新令牌 * * @param refreshToken 刷新令牌字符串 * @returns Promise> 新的令牌对 * * @throws UnauthorizedException 当刷新令牌无效或已过期时 * @throws NotFoundException 当用户不存在或已被禁用时 * * @example * ```typescript * const result = await loginService.refreshAccessToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'); * ``` */ async refreshAccessToken(refreshToken: string): Promise> { try { this.logger.log(`刷新访问令牌尝试`); // 调用核心服务刷新令牌 const tokenPair = await this.loginCoreService.refreshAccessToken(refreshToken); this.logger.log(`访问令牌刷新成功`); return { success: true, data: tokenPair, message: MESSAGES.TOKEN_REFRESH_SUCCESS }; } catch (error) { this.logger.error(`访问令牌刷新失败`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '令牌刷新失败', error_code: ERROR_CODES.TOKEN_REFRESH_FAILED }; } } /** * 调试验证码信息 * 仅用于开发和调试 * * @param email 邮箱地址 * @returns 验证码调试信息 */ async debugVerificationCode(email: string): Promise { try { this.logger.log(`调试验证码信息: ${email}`); const debugInfo = await this.loginCoreService.debugVerificationCode(email); return { success: true, data: debugInfo, message: MESSAGES.DEBUG_INFO_SUCCESS }; } catch (error) { this.logger.error(`获取验证码调试信息失败: ${email}`, error instanceof Error ? error.stack : String(error)); return { success: false, message: error instanceof Error ? error.message : '获取调试信息失败', error_code: ERROR_CODES.DEBUG_VERIFICATION_CODE_FAILED }; } } /** * 验证并更新用户的Zulip API Key * * 功能描述: * 在用户登录时验证其Zulip账号的API Key是否有效,如果无效则重新获取 * * 业务逻辑: * 1. 查找用户的Zulip账号关联 * 2. 从Redis获取API Key * 3. 验证API Key是否有效 * 4. 如果无效,重新生成API Key并更新存储 * * @param user 用户信息 * @returns Promise 是否验证/更新成功 * @private */ private async validateAndUpdateZulipApiKey(user: Users): Promise { const startTime = Date.now(); this.logger.log('开始验证用户Zulip API Key', { operation: 'validateAndUpdateZulipApiKey', userId: user.id.toString(), username: user.username, email: user.email, }); try { // 1. 查找用户的Zulip账号关联 const zulipAccount = await this.zulipAccountsService.findByGameUserId(user.id.toString()); if (!zulipAccount) { this.logger.log('用户没有Zulip账号关联,跳过验证', { operation: 'validateAndUpdateZulipApiKey', userId: user.id.toString(), }); return true; // 没有关联不算错误 } // 2. 从Redis获取API Key const apiKeyResult = await this.apiKeySecurityService.getApiKey(user.id.toString()); if (!apiKeyResult.success || !apiKeyResult.apiKey) { this.logger.warn('用户Zulip API Key不存在,需要重新生成', { operation: 'validateAndUpdateZulipApiKey', userId: user.id.toString(), zulipEmail: zulipAccount.zulipEmail, error: apiKeyResult.message, }); return false; // 需要重新生成 } // 3. 验证API Key是否有效 const validationResult = await this.zulipAccountService.validateZulipAccount( zulipAccount.zulipEmail, apiKeyResult.apiKey ); if (validationResult.success && validationResult.isValid) { this.logger.log('用户Zulip API Key验证成功', { operation: 'validateAndUpdateZulipApiKey', userId: user.id.toString(), zulipEmail: zulipAccount.zulipEmail, }); return true; } // 4. API Key无效,需要重新生成 this.logger.warn('用户Zulip API Key无效,需要重新生成', { operation: 'validateAndUpdateZulipApiKey', userId: user.id.toString(), zulipEmail: zulipAccount.zulipEmail, validationError: validationResult.error, }); return false; // 需要重新生成 } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('验证用户Zulip API Key失败', { operation: 'validateAndUpdateZulipApiKey', userId: user.id.toString(), error: err.message, duration, }, err.stack); return false; } } /** * 重新生成并更新用户的Zulip API Key * * 功能描述: * 使用用户密码重新生成Zulip API Key并更新存储 * * @param user 用户信息 * @param password 用户密码(明文) * @returns Promise 是否更新成功 * @private */ private async regenerateZulipApiKey(user: Users, password: string): Promise { const startTime = Date.now(); this.logger.log('开始重新生成用户Zulip API Key', { operation: 'regenerateZulipApiKey', userId: user.id.toString(), email: user.email, }); try { // 1. 查找用户的Zulip账号关联 const zulipAccount = await this.zulipAccountsService.findByGameUserId(user.id.toString()); if (!zulipAccount) { this.logger.warn('用户没有Zulip账号关联,无法重新生成API Key', { operation: 'regenerateZulipApiKey', userId: user.id.toString(), }); return false; } // 2. 重新生成API Key const apiKeyResult = await this.zulipAccountService.generateApiKeyForUser( zulipAccount.zulipEmail, password ); if (!apiKeyResult.success) { this.logger.error('重新生成Zulip API Key失败', { operation: 'regenerateZulipApiKey', userId: user.id.toString(), zulipEmail: zulipAccount.zulipEmail, error: apiKeyResult.error, }); return false; } // 3. 更新Redis中的API Key await this.apiKeySecurityService.storeApiKey( user.id.toString(), apiKeyResult.apiKey! ); // 注意:不在登录时建立内存关联,Zulip客户端将在WebSocket连接时创建 const duration = Date.now() - startTime; this.logger.log('重新生成Zulip API Key成功', { operation: 'regenerateZulipApiKey', userId: user.id.toString(), zulipEmail: zulipAccount.zulipEmail, duration, }); return true; } catch (error) { const err = error as Error; const duration = Date.now() - startTime; this.logger.error('重新生成Zulip API Key失败', { operation: 'regenerateZulipApiKey', userId: user.id.toString(), error: err.message, duration, }, err.stack); return false; } } }