feat: 添加JWT认证系统和Zulip用户管理服务
- 新增JWT认证守卫(JwtAuthGuard)和当前用户装饰器(CurrentUser) - 添加JWT使用示例和完整的认证流程文档 - 实现Zulip用户管理服务,支持用户查询、验证和管理 - 实现Zulip用户注册服务,支持新用户创建和注册流程 - 添加完整的单元测试覆盖 - 新增真实环境测试脚本,验证Zulip API集成 - 更新.gitignore,排除.kiro目录 主要功能: - JWT令牌验证和用户信息提取 - 用户存在性检查和信息获取 - Zulip API集成和错误处理 - 完整的测试覆盖和文档
This commit is contained in:
539
src/core/zulip/services/user_management.service.ts
Normal file
539
src/core/zulip/services/user_management.service.ts
Normal file
@@ -0,0 +1,539 @@
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user