- 新增JWT认证守卫(JwtAuthGuard)和当前用户装饰器(CurrentUser) - 添加JWT使用示例和完整的认证流程文档 - 实现Zulip用户管理服务,支持用户查询、验证和管理 - 实现Zulip用户注册服务,支持新用户创建和注册流程 - 添加完整的单元测试覆盖 - 新增真实环境测试脚本,验证Zulip API集成 - 更新.gitignore,排除.kiro目录 主要功能: - JWT令牌验证和用户信息提取 - 用户存在性检查和信息获取 - Zulip API集成和错误处理 - 完整的测试覆盖和文档
539 lines
13 KiB
TypeScript
539 lines
13 KiB
TypeScript
/**
|
||
* Zulip用户管理服务
|
||
*
|
||
* 功能描述:
|
||
* - 查询和验证Zulip用户信息
|
||
* - 检查用户是否存在
|
||
* - 获取用户详细信息
|
||
* - 验证用户凭据和权限
|
||
*
|
||
* 主要方法:
|
||
* - checkUserExists(): 检查用户是否存在
|
||
* - getUserInfo(): 获取用户详细信息
|
||
* - validateUserCredentials(): 验证用户凭据
|
||
* - getAllUsers(): 获取所有用户列表
|
||
*
|
||
* 使用场景:
|
||
* - 用户登录时验证用户存在性
|
||
* - 获取用户基本信息
|
||
* - 验证用户权限和状态
|
||
* - 管理员查看用户列表
|
||
*
|
||
* @author angjustinl
|
||
* @version 1.0.0
|
||
* @since 2025-01-06
|
||
*/
|
||
|
||
import { Injectable, Logger, Inject } from '@nestjs/common';
|
||
import { IZulipConfigService } from '../interfaces/zulip-core.interfaces';
|
||
|
||
/**
|
||
* Zulip API响应接口
|
||
*/
|
||
interface ZulipApiResponse {
|
||
result?: 'success' | 'error';
|
||
msg?: string;
|
||
message?: string;
|
||
}
|
||
|
||
/**
|
||
* 用户信息接口
|
||
*/
|
||
interface ZulipUser {
|
||
user_id: number;
|
||
email: string;
|
||
full_name: string;
|
||
is_active: boolean;
|
||
is_admin: boolean;
|
||
is_owner: boolean;
|
||
is_bot: boolean;
|
||
date_joined: string;
|
||
}
|
||
|
||
/**
|
||
* 用户列表响应接口
|
||
*/
|
||
interface ZulipUsersResponse extends ZulipApiResponse {
|
||
members?: ZulipUser[];
|
||
}
|
||
|
||
/**
|
||
* 用户查询请求接口
|
||
*/
|
||
export interface UserQueryRequest {
|
||
email: string;
|
||
}
|
||
|
||
/**
|
||
* 用户信息响应接口
|
||
*/
|
||
export interface UserInfoResponse {
|
||
success: boolean;
|
||
userId?: number;
|
||
email?: string;
|
||
fullName?: string;
|
||
isActive?: boolean;
|
||
isAdmin?: boolean;
|
||
isBot?: boolean;
|
||
dateJoined?: string;
|
||
error?: string;
|
||
}
|
||
|
||
/**
|
||
* 用户验证请求接口
|
||
*/
|
||
export interface UserValidationRequest {
|
||
email: string;
|
||
apiKey?: string;
|
||
}
|
||
|
||
/**
|
||
* 用户验证响应接口
|
||
*/
|
||
export interface UserValidationResponse {
|
||
success: boolean;
|
||
isValid?: boolean;
|
||
userId?: number;
|
||
error?: string;
|
||
}
|
||
|
||
/**
|
||
* 用户列表响应接口
|
||
*/
|
||
export interface UsersListResponse {
|
||
success: boolean;
|
||
users?: Array<{
|
||
userId: number;
|
||
email: string;
|
||
fullName: string;
|
||
isActive: boolean;
|
||
isAdmin: boolean;
|
||
isBot: boolean;
|
||
}>;
|
||
totalCount?: number;
|
||
error?: string;
|
||
}
|
||
|
||
/**
|
||
* Zulip用户管理服务类
|
||
*
|
||
* 职责:
|
||
* - 查询和验证Zulip用户信息
|
||
* - 检查用户是否存在于Zulip服务器
|
||
* - 获取用户详细信息和权限状态
|
||
* - 提供用户管理相关的API接口
|
||
*/
|
||
@Injectable()
|
||
export class UserManagementService {
|
||
private readonly logger = new Logger(UserManagementService.name);
|
||
|
||
constructor(
|
||
@Inject('ZULIP_CONFIG_SERVICE')
|
||
private readonly configService: IZulipConfigService,
|
||
) {
|
||
this.logger.log('UserManagementService初始化完成');
|
||
}
|
||
|
||
/**
|
||
* 检查用户是否存在
|
||
*
|
||
* 功能描述:
|
||
* 通过Zulip API检查指定邮箱的用户是否存在
|
||
*
|
||
* 业务逻辑:
|
||
* 1. 获取所有用户列表
|
||
* 2. 在列表中查找指定邮箱
|
||
* 3. 返回用户存在性结果
|
||
*
|
||
* @param email 用户邮箱
|
||
* @returns Promise<boolean> 是否存在
|
||
*/
|
||
async checkUserExists(email: string): Promise<boolean> {
|
||
const startTime = Date.now();
|
||
|
||
this.logger.log('开始检查用户是否存在', {
|
||
operation: 'checkUserExists',
|
||
email,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
try {
|
||
// 1. 验证邮箱格式
|
||
if (!email || !this.isValidEmail(email)) {
|
||
this.logger.warn('邮箱格式无效', {
|
||
operation: 'checkUserExists',
|
||
email,
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// 2. 获取用户列表
|
||
const usersResult = await this.getAllUsers();
|
||
if (!usersResult.success) {
|
||
this.logger.warn('获取用户列表失败', {
|
||
operation: 'checkUserExists',
|
||
email,
|
||
error: usersResult.error,
|
||
});
|
||
return false;
|
||
}
|
||
|
||
// 3. 检查用户是否存在
|
||
const userExists = usersResult.users?.some(user =>
|
||
user.email.toLowerCase() === email.toLowerCase()
|
||
) || false;
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.log('用户存在性检查完成', {
|
||
operation: 'checkUserExists',
|
||
email,
|
||
exists: userExists,
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
return userExists;
|
||
|
||
} catch (error) {
|
||
const err = error as Error;
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.error('检查用户存在性失败', {
|
||
operation: 'checkUserExists',
|
||
email,
|
||
error: err.message,
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
}, err.stack);
|
||
|
||
return false;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取用户详细信息
|
||
*
|
||
* 功能描述:
|
||
* 根据邮箱获取用户的详细信息
|
||
*
|
||
* @param request 用户查询请求
|
||
* @returns Promise<UserInfoResponse>
|
||
*/
|
||
async getUserInfo(request: UserQueryRequest): Promise<UserInfoResponse> {
|
||
const startTime = Date.now();
|
||
|
||
this.logger.log('开始获取用户信息', {
|
||
operation: 'getUserInfo',
|
||
email: request.email,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
try {
|
||
// 1. 验证请求参数
|
||
if (!request.email || !this.isValidEmail(request.email)) {
|
||
return {
|
||
success: false,
|
||
error: '邮箱格式无效',
|
||
};
|
||
}
|
||
|
||
// 2. 获取用户列表
|
||
const usersResult = await this.getAllUsers();
|
||
if (!usersResult.success) {
|
||
return {
|
||
success: false,
|
||
error: usersResult.error || '获取用户列表失败',
|
||
};
|
||
}
|
||
|
||
// 3. 查找指定用户
|
||
const user = usersResult.users?.find(u =>
|
||
u.email.toLowerCase() === request.email.toLowerCase()
|
||
);
|
||
|
||
if (!user) {
|
||
return {
|
||
success: false,
|
||
error: '用户不存在',
|
||
};
|
||
}
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.log('用户信息获取完成', {
|
||
operation: 'getUserInfo',
|
||
email: request.email,
|
||
userId: user.userId,
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
userId: user.userId,
|
||
email: user.email,
|
||
fullName: user.fullName,
|
||
isActive: user.isActive,
|
||
isAdmin: user.isAdmin,
|
||
isBot: user.isBot,
|
||
};
|
||
|
||
} catch (error) {
|
||
const err = error as Error;
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.error('获取用户信息失败', {
|
||
operation: 'getUserInfo',
|
||
email: request.email,
|
||
error: err.message,
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
}, err.stack);
|
||
|
||
return {
|
||
success: false,
|
||
error: '系统错误,请稍后重试',
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 验证用户凭据
|
||
*
|
||
* 功能描述:
|
||
* 验证用户的API Key是否有效
|
||
*
|
||
* @param request 用户验证请求
|
||
* @returns Promise<UserValidationResponse>
|
||
*/
|
||
async validateUserCredentials(request: UserValidationRequest): Promise<UserValidationResponse> {
|
||
const startTime = Date.now();
|
||
|
||
this.logger.log('开始验证用户凭据', {
|
||
operation: 'validateUserCredentials',
|
||
email: request.email,
|
||
hasApiKey: !!request.apiKey,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
try {
|
||
// 1. 验证请求参数
|
||
if (!request.email || !this.isValidEmail(request.email)) {
|
||
return {
|
||
success: false,
|
||
error: '邮箱格式无效',
|
||
};
|
||
}
|
||
|
||
if (!request.apiKey) {
|
||
return {
|
||
success: false,
|
||
error: 'API Key不能为空',
|
||
};
|
||
}
|
||
|
||
// 2. 使用用户的API Key测试连接
|
||
const isValid = await this.testUserApiKey(request.email, request.apiKey);
|
||
|
||
// 3. 如果API Key有效,获取用户ID
|
||
let userId = undefined;
|
||
if (isValid) {
|
||
const userInfo = await this.getUserInfo({ email: request.email });
|
||
if (userInfo.success) {
|
||
userId = userInfo.userId;
|
||
}
|
||
}
|
||
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.log('用户凭据验证完成', {
|
||
operation: 'validateUserCredentials',
|
||
email: request.email,
|
||
isValid,
|
||
userId,
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
isValid,
|
||
userId,
|
||
};
|
||
|
||
} catch (error) {
|
||
const err = error as Error;
|
||
const duration = Date.now() - startTime;
|
||
|
||
this.logger.error('验证用户凭据失败', {
|
||
operation: 'validateUserCredentials',
|
||
email: request.email,
|
||
error: err.message,
|
||
duration,
|
||
timestamp: new Date().toISOString(),
|
||
}, err.stack);
|
||
|
||
return {
|
||
success: false,
|
||
error: '系统错误,请稍后重试',
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取所有用户列表
|
||
*
|
||
* 功能描述:
|
||
* 从Zulip服务器获取所有用户的列表
|
||
*
|
||
* @returns Promise<UsersListResponse>
|
||
*/
|
||
async getAllUsers(): Promise<UsersListResponse> {
|
||
this.logger.debug('开始获取用户列表', {
|
||
operation: 'getAllUsers',
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
try {
|
||
// 获取Zulip配置
|
||
const config = this.configService.getZulipConfig();
|
||
|
||
// 构建API URL
|
||
const apiUrl = `${config.zulipServerUrl}/api/v1/users`;
|
||
|
||
// 构建认证头
|
||
const auth = Buffer.from(`${config.zulipBotEmail}:${config.zulipBotApiKey}`).toString('base64');
|
||
|
||
// 发送请求
|
||
const response = await fetch(apiUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Basic ${auth}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
if (!response.ok) {
|
||
this.logger.warn('获取用户列表失败', {
|
||
operation: 'getAllUsers',
|
||
status: response.status,
|
||
statusText: response.statusText,
|
||
});
|
||
|
||
return {
|
||
success: false,
|
||
error: `API调用失败: ${response.status} ${response.statusText}`,
|
||
};
|
||
}
|
||
|
||
const data: ZulipUsersResponse = await response.json();
|
||
|
||
// 转换数据格式
|
||
const users = data.members?.map(user => ({
|
||
userId: user.user_id,
|
||
email: user.email,
|
||
fullName: user.full_name,
|
||
isActive: user.is_active,
|
||
isAdmin: user.is_admin,
|
||
isBot: user.is_bot,
|
||
})) || [];
|
||
|
||
this.logger.debug('用户列表获取完成', {
|
||
operation: 'getAllUsers',
|
||
userCount: users.length,
|
||
timestamp: new Date().toISOString(),
|
||
});
|
||
|
||
return {
|
||
success: true,
|
||
users,
|
||
totalCount: users.length,
|
||
};
|
||
|
||
} catch (error) {
|
||
const err = error as Error;
|
||
this.logger.error('获取用户列表异常', {
|
||
operation: 'getAllUsers',
|
||
error: err.message,
|
||
timestamp: new Date().toISOString(),
|
||
}, err.stack);
|
||
|
||
return {
|
||
success: false,
|
||
error: '系统错误,请稍后重试',
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 测试用户API Key是否有效
|
||
*
|
||
* 功能描述:
|
||
* 使用用户的API Key测试是否能够成功调用Zulip API
|
||
*
|
||
* @param email 用户邮箱
|
||
* @param apiKey 用户API Key
|
||
* @returns Promise<boolean> 是否有效
|
||
* @private
|
||
*/
|
||
private async testUserApiKey(email: string, apiKey: string): Promise<boolean> {
|
||
this.logger.debug('测试用户API Key', {
|
||
operation: 'testUserApiKey',
|
||
email,
|
||
});
|
||
|
||
try {
|
||
// 获取Zulip配置
|
||
const config = this.configService.getZulipConfig();
|
||
|
||
// 构建API URL - 使用获取用户自己信息的接口
|
||
const apiUrl = `${config.zulipServerUrl}/api/v1/users/me`;
|
||
|
||
// 使用用户的API Key构建认证头
|
||
const auth = Buffer.from(`${email}:${apiKey}`).toString('base64');
|
||
|
||
// 发送请求
|
||
const response = await fetch(apiUrl, {
|
||
method: 'GET',
|
||
headers: {
|
||
'Authorization': `Basic ${auth}`,
|
||
'Content-Type': 'application/json',
|
||
},
|
||
});
|
||
|
||
const isValid = response.ok;
|
||
|
||
this.logger.debug('API Key测试完成', {
|
||
operation: 'testUserApiKey',
|
||
email,
|
||
isValid,
|
||
status: response.status,
|
||
});
|
||
|
||
return isValid;
|
||
|
||
} catch (error) {
|
||
const err = error as Error;
|
||
this.logger.error('测试API Key异常', {
|
||
operation: 'testUserApiKey',
|
||
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);
|
||
}
|
||
} |