forked from xiangwang25/whale-town-end-v2
751 lines
24 KiB
TypeScript
751 lines
24 KiB
TypeScript
/**
|
||
* 登录业务服务
|
||
*
|
||
* 功能描述:
|
||
* - 处理用户登录相关的业务逻辑和流程控制
|
||
* - 整合核心服务,提供完整的登录功能
|
||
* - 处理业务规则、数据格式化和错误处理
|
||
* - 管理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<any>;
|
||
create(createDto: any): Promise<any>;
|
||
deleteByGameUserId(gameUserId: string): Promise<boolean>;
|
||
}
|
||
|
||
// 常量定义
|
||
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<T = any> {
|
||
/** 是否成功 */
|
||
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<ApiResponse<LoginResponse>> 登录响应
|
||
*
|
||
* @throws BadRequestException 当登录参数无效时
|
||
* @throws UnauthorizedException 当用户凭据错误时
|
||
* @throws InternalServerErrorException 当系统错误时
|
||
*/
|
||
async login(loginRequest: LoginRequest): Promise<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<ApiResponse> {
|
||
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<ApiResponse> {
|
||
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<ApiResponse<LoginResponse>> {
|
||
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<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
|
||
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<ApiResponse<TokenPair>> 新的令牌对
|
||
*
|
||
* @throws UnauthorizedException 当刷新令牌无效或已过期时
|
||
* @throws NotFoundException 当用户不存在或已被禁用时
|
||
*
|
||
* @example
|
||
* ```typescript
|
||
* const result = await loginService.refreshAccessToken('eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...');
|
||
* ```
|
||
*/
|
||
async refreshAccessToken(refreshToken: string): Promise<ApiResponse<TokenPair>> {
|
||
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<any> {
|
||
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<boolean> 是否验证/更新成功
|
||
* @private
|
||
*/
|
||
private async validateAndUpdateZulipApiKey(user: Users): Promise<boolean> {
|
||
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<boolean> 是否更新成功
|
||
* @private
|
||
*/
|
||
private async regenerateZulipApiKey(user: Users, password: string): Promise<boolean> {
|
||
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;
|
||
}
|
||
}
|
||
}
|