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

@@ -0,0 +1,708 @@
/**
* Zulip账号管理核心服务
*
* 功能描述:
* - 自动创建Zulip用户账号
* - 生成API Key并安全存储
* - 处理账号创建失败场景
* - 管理用户账号与游戏账号的关联
*
* 主要方法:
* - createZulipAccount(): 创建新的Zulip用户账号
* - generateApiKey(): 为用户生成API Key
* - validateZulipAccount(): 验证Zulip账号有效性
* - linkGameAccount(): 关联游戏账号与Zulip账号
*
* 使用场景:
* - 用户注册时自动创建Zulip账号
* - API Key管理和更新
* - 账号关联和映射存储
*
* @author angjustinl
* @version 1.0.0
* @since 2025-01-05
*/
import { Injectable, Logger } from '@nestjs/common';
import { ZulipClientConfig } from '../interfaces/zulip-core.interfaces';
/**
* Zulip账号创建请求接口
*/
export interface CreateZulipAccountRequest {
email: string;
fullName: string;
password?: string;
shortName?: string;
}
/**
* Zulip账号创建结果接口
*/
export interface CreateZulipAccountResult {
success: boolean;
userId?: number;
email?: string;
apiKey?: string;
error?: string;
errorCode?: string;
}
/**
* API Key生成结果接口
*/
export interface GenerateApiKeyResult {
success: boolean;
apiKey?: string;
error?: string;
}
/**
* 账号验证结果接口
*/
export interface ValidateAccountResult {
success: boolean;
isValid?: boolean;
userInfo?: any;
error?: string;
}
/**
* 账号关联信息接口
*/
export interface AccountLinkInfo {
gameUserId: string;
zulipUserId: number;
zulipEmail: string;
zulipApiKey: string;
createdAt: Date;
lastVerified?: Date;
isActive: boolean;
}
/**
* Zulip账号管理服务类
*
* 职责:
* - 处理Zulip用户账号的创建和管理
* - 管理API Key的生成和存储
* - 维护游戏账号与Zulip账号的关联关系
* - 提供账号验证和状态检查功能
*
* 主要方法:
* - createZulipAccount(): 创建新的Zulip用户账号
* - generateApiKey(): 为现有用户生成API Key
* - validateZulipAccount(): 验证Zulip账号有效性
* - linkGameAccount(): 建立游戏账号与Zulip账号的关联
* - unlinkGameAccount(): 解除账号关联
*
* 使用场景:
* - 用户注册流程中自动创建Zulip账号
* - API Key管理和更新
* - 账号状态监控和维护
* - 跨平台账号同步
*/
@Injectable()
export class ZulipAccountService {
private readonly logger = new Logger(ZulipAccountService.name);
private adminClient: any = null;
private readonly accountLinks = new Map<string, AccountLinkInfo>();
constructor() {
this.logger.log('ZulipAccountService初始化完成');
}
/**
* 初始化管理员客户端
*
* 功能描述:
* 使用管理员凭证初始化Zulip客户端用于创建用户账号
*
* @param adminConfig 管理员配置
* @returns Promise<boolean> 是否初始化成功
*/
async initializeAdminClient(adminConfig: ZulipClientConfig): Promise<boolean> {
this.logger.log('初始化Zulip管理员客户端', {
operation: 'initializeAdminClient',
realm: adminConfig.realm,
timestamp: new Date().toISOString(),
});
try {
// 动态导入zulip-js
const zulipInit = await this.loadZulipModule();
// 创建管理员客户端
this.adminClient = await zulipInit({
username: adminConfig.username,
apiKey: adminConfig.apiKey,
realm: adminConfig.realm,
});
// 验证管理员权限
const profile = await this.adminClient.users.me.getProfile();
if (profile.result !== 'success') {
throw new Error(`管理员客户端验证失败: ${profile.msg || '未知错误'}`);
}
this.logger.log('管理员客户端初始化成功', {
operation: 'initializeAdminClient',
adminEmail: profile.email,
isAdmin: profile.is_admin,
timestamp: new Date().toISOString(),
});
return true;
} catch (error) {
const err = error as Error;
this.logger.error('管理员客户端初始化失败', {
operation: 'initializeAdminClient',
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 创建Zulip用户账号
*
* 功能描述:
* 使用管理员权限在Zulip服务器上创建新的用户账号
*
* 业务逻辑:
* 1. 验证管理员客户端是否已初始化
* 2. 检查邮箱是否已存在
* 3. 生成用户密码(如果未提供)
* 4. 调用Zulip API创建用户
* 5. 为新用户生成API Key
* 6. 返回创建结果
*
* @param request 账号创建请求
* @returns Promise<CreateZulipAccountResult> 创建结果
*/
async createZulipAccount(request: CreateZulipAccountRequest): Promise<CreateZulipAccountResult> {
const startTime = Date.now();
this.logger.log('开始创建Zulip账号', {
operation: 'createZulipAccount',
email: request.email,
fullName: request.fullName,
timestamp: new Date().toISOString(),
});
try {
// 1. 验证管理员客户端
if (!this.adminClient) {
throw new Error('管理员客户端未初始化');
}
// 2. 验证请求参数
if (!request.email || !request.email.trim()) {
throw new Error('邮箱地址不能为空');
}
if (!request.fullName || !request.fullName.trim()) {
throw new Error('用户全名不能为空');
}
// 3. 检查邮箱格式
if (!this.isValidEmail(request.email)) {
throw new Error('邮箱格式无效');
}
// 4. 检查用户是否已存在
const existingUser = await this.checkUserExists(request.email);
if (existingUser) {
this.logger.warn('用户已存在', {
operation: 'createZulipAccount',
email: request.email,
});
return {
success: false,
error: '用户已存在',
errorCode: 'USER_ALREADY_EXISTS',
};
}
// 5. 生成密码(如果未提供)
const password = request.password || this.generateRandomPassword();
const shortName = request.shortName || this.generateShortName(request.email);
// 6. 创建用户参数
const createParams = {
email: request.email,
password: password,
full_name: request.fullName,
short_name: shortName,
};
// 7. 调用Zulip API创建用户
const createResponse = await this.adminClient.users.create(createParams);
if (createResponse.result !== 'success') {
this.logger.warn('Zulip用户创建失败', {
operation: 'createZulipAccount',
email: request.email,
error: createResponse.msg,
});
return {
success: false,
error: createResponse.msg || '用户创建失败',
errorCode: 'ZULIP_CREATE_FAILED',
};
}
// 8. 为新用户生成API Key
const apiKeyResult = await this.generateApiKeyForUser(request.email, password);
if (!apiKeyResult.success) {
this.logger.warn('API Key生成失败但用户已创建', {
operation: 'createZulipAccount',
email: request.email,
error: apiKeyResult.error,
});
// 用户已创建但API Key生成失败
return {
success: true,
userId: createResponse.user_id,
email: request.email,
error: `用户创建成功但API Key生成失败: ${apiKeyResult.error}`,
errorCode: 'API_KEY_GENERATION_FAILED',
};
}
const duration = Date.now() - startTime;
this.logger.log('Zulip账号创建成功', {
operation: 'createZulipAccount',
email: request.email,
userId: createResponse.user_id,
hasApiKey: !!apiKeyResult.apiKey,
duration,
timestamp: new Date().toISOString(),
});
return {
success: true,
userId: createResponse.user_id,
email: request.email,
apiKey: apiKeyResult.apiKey,
};
} catch (error) {
const err = error as Error;
const duration = Date.now() - startTime;
this.logger.error('创建Zulip账号失败', {
operation: 'createZulipAccount',
email: request.email,
error: err.message,
duration,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
error: err.message,
errorCode: 'ACCOUNT_CREATION_FAILED',
};
}
}
/**
* 为用户生成API Key
*
* 功能描述:
* 使用用户凭证获取API Key
*
* @param email 用户邮箱
* @param password 用户密码
* @returns Promise<GenerateApiKeyResult> 生成结果
*/
async generateApiKeyForUser(email: string, password: string): Promise<GenerateApiKeyResult> {
this.logger.log('为用户生成API Key', {
operation: 'generateApiKeyForUser',
email,
timestamp: new Date().toISOString(),
});
try {
// 动态导入zulip-js
const zulipInit = await this.loadZulipModule();
// 使用用户凭证获取API Key
const userClient = await zulipInit({
username: email,
password: password,
realm: this.getRealmFromAdminClient(),
});
// 验证客户端并获取API Key
const profile = await userClient.users.me.getProfile();
if (profile.result !== 'success') {
throw new Error(`API Key获取失败: ${profile.msg || '未知错误'}`);
}
// 从客户端配置中提取API Key
const apiKey = userClient.config?.apiKey;
if (!apiKey) {
throw new Error('无法从客户端配置中获取API Key');
}
this.logger.log('API Key生成成功', {
operation: 'generateApiKeyForUser',
email,
timestamp: new Date().toISOString(),
});
return {
success: true,
apiKey: apiKey,
};
} catch (error) {
const err = error as Error;
this.logger.error('API Key生成失败', {
operation: 'generateApiKeyForUser',
email,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
error: err.message,
};
}
}
/**
* 验证Zulip账号有效性
*
* 功能描述:
* 验证指定的Zulip账号是否存在且有效
*
* @param email 用户邮箱
* @param apiKey 用户API Key可选
* @returns Promise<ValidateAccountResult> 验证结果
*/
async validateZulipAccount(email: string, apiKey?: string): Promise<ValidateAccountResult> {
this.logger.log('验证Zulip账号', {
operation: 'validateZulipAccount',
email,
hasApiKey: !!apiKey,
timestamp: new Date().toISOString(),
});
try {
if (apiKey) {
// 使用API Key验证
const zulipInit = await this.loadZulipModule();
const userClient = await zulipInit({
username: email,
apiKey: apiKey,
realm: this.getRealmFromAdminClient(),
});
const profile = await userClient.users.me.getProfile();
if (profile.result === 'success') {
this.logger.log('账号验证成功API Key', {
operation: 'validateZulipAccount',
email,
userId: profile.user_id,
});
return {
success: true,
isValid: true,
userInfo: profile,
};
} else {
return {
success: true,
isValid: false,
error: profile.msg || 'API Key验证失败',
};
}
} else {
// 仅检查用户是否存在
const userExists = await this.checkUserExists(email);
this.logger.log('账号存在性检查完成', {
operation: 'validateZulipAccount',
email,
exists: userExists,
});
return {
success: true,
isValid: userExists,
};
}
} catch (error) {
const err = error as Error;
this.logger.error('账号验证失败', {
operation: 'validateZulipAccount',
email,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return {
success: false,
error: err.message,
};
}
}
/**
* 关联游戏账号与Zulip账号
*
* 功能描述:
* 建立游戏用户ID与Zulip账号的映射关系
*
* @param gameUserId 游戏用户ID
* @param zulipUserId Zulip用户ID
* @param zulipEmail Zulip邮箱
* @param zulipApiKey Zulip API Key
* @returns Promise<boolean> 是否关联成功
*/
async linkGameAccount(
gameUserId: string,
zulipUserId: number,
zulipEmail: string,
zulipApiKey: string,
): Promise<boolean> {
this.logger.log('关联游戏账号与Zulip账号', {
operation: 'linkGameAccount',
gameUserId,
zulipUserId,
zulipEmail,
timestamp: new Date().toISOString(),
});
try {
// 验证参数
if (!gameUserId || !zulipUserId || !zulipEmail || !zulipApiKey) {
throw new Error('关联参数不完整');
}
// 创建关联信息
const linkInfo: AccountLinkInfo = {
gameUserId,
zulipUserId,
zulipEmail,
zulipApiKey,
createdAt: new Date(),
isActive: true,
};
// 存储关联信息(实际项目中应存储到数据库)
this.accountLinks.set(gameUserId, linkInfo);
this.logger.log('账号关联成功', {
operation: 'linkGameAccount',
gameUserId,
zulipUserId,
zulipEmail,
timestamp: new Date().toISOString(),
});
return true;
} catch (error) {
const err = error as Error;
this.logger.error('账号关联失败', {
operation: 'linkGameAccount',
gameUserId,
zulipUserId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 解除游戏账号与Zulip账号的关联
*
* @param gameUserId 游戏用户ID
* @returns Promise<boolean> 是否解除成功
*/
async unlinkGameAccount(gameUserId: string): Promise<boolean> {
this.logger.log('解除账号关联', {
operation: 'unlinkGameAccount',
gameUserId,
timestamp: new Date().toISOString(),
});
try {
const linkInfo = this.accountLinks.get(gameUserId);
if (linkInfo) {
linkInfo.isActive = false;
this.accountLinks.delete(gameUserId);
this.logger.log('账号关联解除成功', {
operation: 'unlinkGameAccount',
gameUserId,
zulipEmail: linkInfo.zulipEmail,
});
}
return true;
} catch (error) {
const err = error as Error;
this.logger.error('解除账号关联失败', {
operation: 'unlinkGameAccount',
gameUserId,
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
return false;
}
}
/**
* 获取游戏账号的Zulip关联信息
*
* @param gameUserId 游戏用户ID
* @returns AccountLinkInfo | null 关联信息
*/
getAccountLink(gameUserId: string): AccountLinkInfo | null {
return this.accountLinks.get(gameUserId) || null;
}
/**
* 获取所有账号关联信息
*
* @returns AccountLinkInfo[] 所有关联信息
*/
getAllAccountLinks(): AccountLinkInfo[] {
return Array.from(this.accountLinks.values()).filter(link => link.isActive);
}
/**
* 检查用户是否已存在
*
* @param email 用户邮箱
* @returns Promise<boolean> 用户是否存在
* @private
*/
private async checkUserExists(email: string): Promise<boolean> {
try {
if (!this.adminClient) {
return false;
}
// 获取所有用户列表
const usersResponse = await this.adminClient.users.retrieve();
if (usersResponse.result === 'success') {
const users = usersResponse.members || [];
return users.some((user: any) => user.email === email);
}
return false;
} catch (error) {
const err = error as Error;
this.logger.warn('检查用户存在性失败', {
operation: 'checkUserExists',
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);
}
/**
* 生成随机密码
*
* @returns string 随机密码
* @private
*/
private generateRandomPassword(): string {
const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*';
let password = '';
for (let i = 0; i < 12; i++) {
password += chars.charAt(Math.floor(Math.random() * chars.length));
}
return password;
}
/**
* 从邮箱生成短名称
*
* @param email 邮箱地址
* @returns string 短名称
* @private
*/
private generateShortName(email: string): string {
const localPart = email.split('@')[0];
// 移除特殊字符,只保留字母数字和下划线
return localPart.replace(/[^a-zA-Z0-9_]/g, '').toLowerCase();
}
/**
* 从管理员客户端获取Realm
*
* @returns string Realm URL
* @private
*/
private getRealmFromAdminClient(): string {
if (!this.adminClient || !this.adminClient.config) {
throw new Error('管理员客户端未初始化或配置缺失');
}
return this.adminClient.config.realm;
}
/**
* 动态加载zulip-js模块
*
* @returns Promise<any> zulip-js初始化函数
* @private
*/
private async loadZulipModule(): Promise<any> {
try {
// 使用动态导入加载zulip-js
const zulipModule = await import('zulip-js');
return zulipModule.default || zulipModule;
} catch (error) {
const err = error as Error;
this.logger.error('加载zulip-js模块失败', {
operation: 'loadZulipModule',
error: err.message,
timestamp: new Date().toISOString(),
}, err.stack);
throw new Error(`加载zulip-js模块失败: ${err.message}`);
}
}
}