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
This commit is contained in:
angjustinl
2026-01-05 17:41:54 +08:00
parent 9cb172d645
commit 6ad8d80449
14 changed files with 2698 additions and 38 deletions

View File

@@ -16,9 +16,12 @@
* @since 2025-12-17
*/
import { Injectable, Logger } from '@nestjs/common';
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';
/**
* 登录响应数据接口
@@ -65,6 +68,10 @@ export class LoginService {
constructor(
private readonly loginCoreService: LoginCoreService,
private readonly zulipAccountService: ZulipAccountService,
@Inject('ZulipAccountsRepository')
private readonly zulipAccountsRepository: ZulipAccountsRepository,
private readonly apiKeySecurityService: ApiKeySecurityService,
) {}
/**
@@ -116,36 +123,106 @@ export class LoginService {
* @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: '注册成功'
message: zulipAccountCreated ? '注册成功Zulip账号已同步创建' : '注册成功'
};
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`);
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: '注册成功'
message: response.message
};
} catch (error) {
this.logger.error(`用户注册失败: ${registerRequest.username}`, error instanceof Error ? error.stack : String(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: error instanceof Error ? error.message : '注册失败',
message: err.message || '注册失败',
error_code: 'REGISTER_FAILED'
};
}
@@ -592,4 +669,171 @@ export class LoginService {
};
}
}
/**
* 初始化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;
}
}
}