Files
whale-town-end/src/business/auth/services/login.service.ts
angjustinl 6ad8d80449 feat(zulip): Add Zulip account management and integrate with auth system
- Add ZulipAccountsEntity, repository, and module for persistent Zulip account storage
- Create ZulipAccountService in core layer for managing Zulip account lifecycle
- Integrate Zulip account creation into login flow via LoginService
- Add comprehensive test suite for Zulip account creation during user registration
- Create quick test script for validating registered user Zulip integration
- Update UsersEntity to support Zulip account associations
- Update auth module to include Zulip and ZulipAccounts dependencies
- Fix WebSocket connection protocol from ws:// to wss:// in API documentation
- Enhance LoginCoreService to coordinate Zulip account provisioning during authentication
2026-01-05 17:41:54 +08:00

839 lines
26 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 登录业务服务
*
* 功能描述:
* - 处理登录相关的业务逻辑和流程控制
* - 整合核心服务,提供完整的业务功能
* - 处理业务规则、数据格式化和错误处理
*
* 职责分离:
* - 专注于业务流程和规则实现
* - 调用核心服务完成具体功能
* - 为控制器层提供业务接口
*
* @author moyin angjustinl
* @version 1.0.0
* @since 2025-12-17
*/
import { Injectable, Logger, Inject } from '@nestjs/common';
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, AuthResult, VerificationCodeLoginRequest } from '../../../core/login_core/login_core.service';
import { Users } from '../../../core/db/users/users.entity';
import { ZulipAccountService } from '../../../core/zulip/services/zulip_account.service';
import { ZulipAccountsRepository } from '../../../core/db/zulip_accounts/zulip_accounts.repository';
import { ApiKeySecurityService } from '../../../core/zulip/services/api_key_security.service';
/**
* 登录响应数据接口
*/
export interface LoginResponse {
/** 用户信息 */
user: {
id: string;
username: string;
nickname: string;
email?: string;
phone?: string;
avatar_url?: string;
role: number;
created_at: Date;
};
/** 访问令牌实际应用中应生成JWT */
access_token: string;
/** 刷新令牌 */
refresh_token?: 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('ZulipAccountsRepository')
private readonly zulipAccountsRepository: ZulipAccountsRepository,
private readonly apiKeySecurityService: ApiKeySecurityService,
) {}
/**
* 用户登录
*
* @param loginRequest 登录请求
* @returns 登录响应
*/
async login(loginRequest: LoginRequest): Promise<ApiResponse<LoginResponse>> {
try {
this.logger.log(`用户登录尝试: ${loginRequest.identifier}`);
// 调用核心服务进行认证
const authResult = await this.loginCoreService.login(loginRequest);
// 生成访问令牌实际应用中应使用JWT
const accessToken = this.generateAccessToken(authResult.user);
// 格式化响应数据
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: accessToken,
is_new_user: authResult.isNewUser,
message: '登录成功'
};
this.logger.log(`用户登录成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
return {
success: true,
data: response,
message: '登录成功'
};
} 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: 'LOGIN_FAILED'
};
}
}
/**
* 用户注册
*
* @param registerRequest 注册请求
* @returns 注册响应
*/
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
const startTime = Date.now();
try {
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
// 1. 初始化Zulip管理员客户端
await this.initializeZulipAdminClient();
// 2. 调用核心服务进行注册
const authResult = await this.loginCoreService.register(registerRequest);
// 3. 创建Zulip账号使用相同的邮箱和密码
let zulipAccountCreated = false;
try {
if (registerRequest.email && registerRequest.password) {
await this.createZulipAccountForUser(authResult.user, registerRequest.password);
zulipAccountCreated = true;
this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, {
operation: 'register',
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
} else {
this.logger.warn(`跳过Zulip账号创建缺少邮箱或密码`, {
operation: 'register',
username: registerRequest.username,
hasEmail: !!registerRequest.email,
hasPassword: !!registerRequest.password,
});
}
} catch (zulipError) {
const err = zulipError as Error;
this.logger.error(`Zulip账号创建失败回滚用户注册`, {
operation: 'register',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
zulipError: err.message,
}, err.stack);
// 回滚游戏用户注册
try {
await this.loginCoreService.deleteUser(authResult.user.id);
this.logger.log(`用户注册回滚成功: ${registerRequest.username}`);
} catch (rollbackError) {
const rollbackErr = rollbackError as Error;
this.logger.error(`用户注册回滚失败`, {
operation: 'register',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
rollbackError: rollbackErr.message,
}, rollbackErr.stack);
}
// 抛出原始错误
throw new Error(`注册失败Zulip账号创建失败 - ${err.message}`);
}
// 4. 生成访问令牌
const accessToken = this.generateAccessToken(authResult.user);
// 5. 格式化响应数据
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: accessToken,
is_new_user: true,
message: zulipAccountCreated ? '注册成功Zulip账号已同步创建' : '注册成功'
};
const duration = Date.now() - startTime;
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, {
operation: 'register',
gameUserId: authResult.user.id.toString(),
username: authResult.user.username,
zulipAccountCreated,
duration,
timestamp: new Date().toISOString(),
});
return {
success: true,
data: response,
message: response.message
};
} catch (error) {
const duration = Date.now() - startTime;
const err = error as Error;
this.logger.error(`用户注册失败: ${registerRequest.username}`, {
operation: 'register',
username: registerRequest.username,
error: err.message,
duration,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
message: err.message || '注册失败',
error_code: 'REGISTER_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);
// 生成访问令牌
const accessToken = this.generateAccessToken(authResult.user);
// 格式化响应数据
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: accessToken,
is_new_user: authResult.isNewUser,
message: authResult.isNewUser ? 'GitHub账户绑定成功' : 'GitHub登录成功'
};
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: '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}`);
// 根据是否为测试模式返回不同的状态和消息 by angjustinl 2025-12-17
if (result.isTestMode) {
// 测试模式:验证码生成但未真实发送
return {
success: false, // 测试模式下不算真正成功
data: {
verification_code: result.code,
is_test_mode: true
},
message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
error_code: 'TEST_MODE_ONLY'
};
} else {
// 真实发送模式
return {
success: true,
data: {
is_test_mode: false
},
message: '验证码已发送,请查收'
};
}
} catch (error) {
this.logger.error(`发送密码重置验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '发送验证码失败',
error_code: '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: '密码重置成功'
};
} 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: '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: '密码修改成功'
};
} 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: 'CHANGE_PASSWORD_FAILED'
};
}
}
/**
* 发送邮箱验证码
*
* @param email 邮箱地址
* @returns 响应结果
*/
async sendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
try {
this.logger.log(`发送邮箱验证码: ${email}`);
// 调用核心服务发送验证码
const result = await this.loginCoreService.sendEmailVerification(email);
this.logger.log(`邮箱验证码已发送: ${email}`);
// 根据是否为测试模式返回不同的状态和消息
if (result.isTestMode) {
// 测试模式:验证码生成但未真实发送
return {
success: false, // 测试模式下不算真正成功
data: {
verification_code: result.code,
is_test_mode: true
},
message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
error_code: 'TEST_MODE_ONLY'
};
} else {
// 真实发送模式
return {
success: true,
data: {
is_test_mode: false
},
message: '验证码已发送,请查收邮件'
};
}
} catch (error) {
this.logger.error(`发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '发送验证码失败',
error_code: 'SEND_EMAIL_VERIFICATION_FAILED'
};
}
}
/**
* 验证邮箱验证码
*
* @param email 邮箱地址
* @param code 验证码
* @returns 响应结果
*/
async verifyEmailCode(email: string, code: string): Promise<ApiResponse> {
try {
this.logger.log(`验证邮箱验证码: ${email}`);
// 调用核心服务验证验证码
const isValid = await this.loginCoreService.verifyEmailCode(email, code);
if (isValid) {
this.logger.log(`邮箱验证成功: ${email}`);
return {
success: true,
message: '邮箱验证成功'
};
} else {
return {
success: false,
message: '验证码错误',
error_code: 'INVALID_VERIFICATION_CODE'
};
}
} catch (error) {
this.logger.error(`邮箱验证失败: ${email}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '邮箱验证失败',
error_code: 'EMAIL_VERIFICATION_FAILED'
};
}
}
/**
* 重新发送邮箱验证码
*
* @param email 邮箱地址
* @returns 响应结果
*/
async resendEmailVerification(email: string): Promise<ApiResponse<{ verification_code?: string; is_test_mode?: boolean }>> {
try {
this.logger.log(`重新发送邮箱验证码: ${email}`);
// 调用核心服务重新发送验证码
const result = await this.loginCoreService.resendEmailVerification(email);
this.logger.log(`邮箱验证码已重新发送: ${email}`);
// 根据是否为测试模式返回不同的状态和消息
if (result.isTestMode) {
// 测试模式:验证码生成但未真实发送
return {
success: false, // 测试模式下不算真正成功
data: {
verification_code: result.code,
is_test_mode: true
},
message: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
error_code: 'TEST_MODE_ONLY'
};
} else {
// 真实发送模式
return {
success: true,
data: {
is_test_mode: false
},
message: '验证码已重新发送,请查收邮件'
};
}
} catch (error) {
this.logger.error(`重新发送邮箱验证码失败: ${email}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '重新发送验证码失败',
error_code: 'RESEND_EMAIL_VERIFICATION_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 user 用户信息
* @returns 访问令牌
*/
private generateAccessToken(user: Users): string {
// 实际应用中应使用JWT库生成真正的JWT令牌
// 这里仅用于演示,生成一个简单的令牌
const payload = {
userId: user.id.toString(),
username: user.username,
role: user.role,
timestamp: Date.now()
};
// 简单的Base64编码实际应用中应使用JWT
return Buffer.from(JSON.stringify(payload)).toString('base64');
}
/**
* 验证码登录
*
* @param loginRequest 验证码登录请求
* @returns 登录响应
*/
async verificationCodeLogin(loginRequest: VerificationCodeLoginRequest): Promise<ApiResponse<LoginResponse>> {
try {
this.logger.log(`验证码登录尝试: ${loginRequest.identifier}`);
// 调用核心服务进行验证码认证
const authResult = await this.loginCoreService.verificationCodeLogin(loginRequest);
// 生成访问令牌
const accessToken = this.generateAccessToken(authResult.user);
// 格式化响应数据
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: accessToken,
is_new_user: authResult.isNewUser,
message: '验证码登录成功'
};
this.logger.log(`验证码登录成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
return {
success: true,
data: response,
message: '验证码登录成功'
};
} 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: '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: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
error_code: 'TEST_MODE_ONLY'
};
} else {
// 真实发送模式
return {
success: true,
data: {
is_test_mode: false
},
message: '验证码已发送,请查收'
};
}
} catch (error) {
this.logger.error(`发送登录验证码失败: ${identifier}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '发送验证码失败',
error_code: 'SEND_LOGIN_CODE_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: '调试信息获取成功'
};
} catch (error) {
this.logger.error(`获取验证码调试信息失败: ${email}`, error instanceof Error ? error.stack : String(error));
return {
success: false,
message: error instanceof Error ? error.message : '获取调试信息失败',
error_code: 'DEBUG_VERIFICATION_CODE_FAILED'
};
}
}
/**
* 初始化Zulip管理员客户端
*
* 功能描述:
* 使用环境变量中的管理员凭证初始化Zulip客户端
*
* 业务逻辑:
* 1. 从环境变量获取管理员配置
* 2. 验证配置完整性
* 3. 初始化ZulipAccountService的管理员客户端
*
* @throws Error 当配置缺失或初始化失败时
* @private
*/
private async initializeZulipAdminClient(): Promise<void> {
try {
// 从环境变量获取管理员配置
const adminConfig = {
realm: process.env.ZULIP_SERVER_URL || '',
username: process.env.ZULIP_BOT_EMAIL || '',
apiKey: process.env.ZULIP_BOT_API_KEY || '',
};
// 验证配置完整性
if (!adminConfig.realm || !adminConfig.username || !adminConfig.apiKey) {
throw new Error('Zulip管理员配置不完整请检查环境变量 ZULIP_SERVER_URL, ZULIP_BOT_EMAIL, ZULIP_BOT_API_KEY');
}
// 初始化管理员客户端
const initialized = await this.zulipAccountService.initializeAdminClient(adminConfig);
if (!initialized) {
throw new Error('Zulip管理员客户端初始化失败');
}
this.logger.log('Zulip管理员客户端初始化成功', {
operation: 'initializeZulipAdminClient',
realm: adminConfig.realm,
adminEmail: adminConfig.username,
});
} catch (error) {
const err = error as Error;
this.logger.error('Zulip管理员客户端初始化失败', {
operation: 'initializeZulipAdminClient',
error: err.message,
}, err.stack);
throw error;
}
}
/**
* 为用户创建Zulip账号
*
* 功能描述:
* 为新注册的游戏用户创建对应的Zulip账号并建立关联
*
* 业务逻辑:
* 1. 使用相同的邮箱和密码创建Zulip账号
* 2. 加密存储API Key
* 3. 在数据库中建立关联关系
* 4. 处理创建失败的情况
*
* @param gameUser 游戏用户信息
* @param password 用户密码(明文)
* @throws Error 当Zulip账号创建失败时
* @private
*/
private async createZulipAccountForUser(gameUser: Users, password: string): Promise<void> {
const startTime = Date.now();
this.logger.log('开始为用户创建Zulip账号', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
email: gameUser.email,
nickname: gameUser.nickname,
});
try {
// 1. 检查是否已存在Zulip账号关联
const existingAccount = await this.zulipAccountsRepository.findByGameUserId(gameUser.id);
if (existingAccount) {
this.logger.warn('用户已存在Zulip账号关联跳过创建', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
existingZulipUserId: existingAccount.zulipUserId,
});
return;
}
// 2. 创建Zulip账号
const createResult = await this.zulipAccountService.createZulipAccount({
email: gameUser.email,
fullName: gameUser.nickname,
password: password,
});
if (!createResult.success) {
throw new Error(createResult.error || 'Zulip账号创建失败');
}
// 3. 存储API Key
if (createResult.apiKey) {
await this.apiKeySecurityService.storeApiKey(
gameUser.id.toString(),
createResult.apiKey
);
}
// 4. 在数据库中创建关联记录
await this.zulipAccountsRepository.create({
gameUserId: gameUser.id,
zulipUserId: createResult.userId!,
zulipEmail: createResult.email!,
zulipFullName: gameUser.nickname,
zulipApiKeyEncrypted: createResult.apiKey ? 'stored_in_redis' : '', // 标记API Key已存储在Redis中
status: 'active',
});
// 5. 建立游戏账号与Zulip账号的内存关联用于当前会话
if (createResult.apiKey) {
await this.zulipAccountService.linkGameAccount(
gameUser.id.toString(),
createResult.userId!,
createResult.email!,
createResult.apiKey
);
}
const duration = Date.now() - startTime;
this.logger.log('Zulip账号创建和关联成功', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
zulipUserId: createResult.userId,
zulipEmail: createResult.email,
hasApiKey: !!createResult.apiKey,
duration,
});
} catch (error) {
const err = error as Error;
const duration = Date.now() - startTime;
this.logger.error('为用户创建Zulip账号失败', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
email: gameUser.email,
error: err.message,
duration,
}, err.stack);
// 清理可能创建的部分数据
try {
await this.zulipAccountsRepository.deleteByGameUserId(gameUser.id);
} catch (cleanupError) {
this.logger.warn('清理Zulip账号关联数据失败', {
operation: 'createZulipAccountForUser',
gameUserId: gameUser.id.toString(),
cleanupError: (cleanupError as Error).message,
});
}
throw error;
}
}
}