Files
whale-town-end/src/core/zulip/services/zulip_account.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

708 lines
18 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.
/**
* 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}`);
}
}
}