CRITICAL ISSUES: Database management service with major problems

WARNING: This commit contains code with significant issues that need immediate attention:

1. Type Safety Issues:
   - Unused import ZulipAccountsService causing compilation warnings
   - Implicit 'any' type in formatZulipAccount method parameter
   - Type inconsistencies in service injections

2. Service Integration Problems:
   - Inconsistent service interface usage
   - Missing proper type definitions for injected services
   - Potential runtime errors due to type mismatches

3. Code Quality Issues:
   - Violation of TypeScript strict mode requirements
   - Inconsistent error handling patterns
   - Missing proper interface implementations

 Files affected:
   - src/business/admin/database_management.service.ts (main issue)
   - Multiple test files and service implementations
   - Configuration and documentation updates

 Next steps required:
   1. Fix TypeScript compilation errors
   2. Implement proper type safety
   3. Resolve service injection inconsistencies
   4. Add comprehensive error handling
   5. Update tests to match new implementations

 Impact: High - affects admin functionality and system stability
 Priority: Urgent - requires immediate review and fixes

Author: moyin
Date: 2026-01-10
This commit is contained in:
moyin
2026-01-10 19:27:28 +08:00
parent f4ce162a38
commit d04ab7f75f
40 changed files with 5766 additions and 3519 deletions

View File

@@ -156,7 +156,7 @@ export class LoginController {
status: 429,
description: '登录尝试过于频繁'
})
@Throttle(ThrottlePresets.LOGIN)
@Throttle(ThrottlePresets.LOGIN_PER_ACCOUNT)
@Timeout(TimeoutPresets.NORMAL)
@Post('login')
@UsePipes(new ValidationPipe({ transform: true }))
@@ -408,7 +408,7 @@ export class LoginController {
status: 429,
description: '发送频率过高'
})
@Throttle(ThrottlePresets.SEND_CODE)
@Throttle(ThrottlePresets.SEND_CODE_PER_EMAIL)
@Timeout(TimeoutPresets.EMAIL_SEND)
@Post('send-email-verification')
@UsePipes(new ValidationPipe({ transform: true }))

View File

@@ -26,10 +26,15 @@ import { Injectable, Logger, Inject } from '@nestjs/common';
import { LoginCoreService, LoginRequest, RegisterRequest, GitHubOAuthRequest, PasswordResetRequest, VerificationCodeLoginRequest, TokenPair } from '../../core/login_core/login_core.service';
import { Users } from '../../core/db/users/users.entity';
import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service';
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
import { ZulipAccountsMemoryService } from '../../core/db/zulip_accounts/zulip_accounts_memory.service';
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
// Import the interface types we need
interface IZulipAccountsService {
findByGameUserId(gameUserId: string, includeGameUser?: boolean): Promise<any>;
create(createDto: any): Promise<any>;
deleteByGameUserId(gameUserId: string): Promise<boolean>;
}
// 常量定义
const ERROR_CODES = {
LOGIN_FAILED: 'LOGIN_FAILED',
@@ -120,8 +125,7 @@ export class LoginService {
constructor(
private readonly loginCoreService: LoginCoreService,
private readonly zulipAccountService: ZulipAccountService,
@Inject('ZulipAccountsService')
private readonly zulipAccountsService: ZulipAccountsService | ZulipAccountsMemoryService,
@Inject('ZulipAccountsService') private readonly zulipAccountsService: IZulipAccountsService,
private readonly apiKeySecurityService: ApiKeySecurityService,
) {}
@@ -215,25 +219,88 @@ export class LoginService {
*/
async register(registerRequest: RegisterRequest): Promise<ApiResponse<LoginResponse>> {
const startTime = Date.now();
const operationId = `register_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
try {
this.logger.log(`用户注册尝试: ${registerRequest.username}`);
this.logger.log(`[${operationId}] 步骤1: 开始用户注册流程`, {
operation: 'register',
operationId,
username: registerRequest.username,
email: registerRequest.email,
hasPassword: !!registerRequest.password,
timestamp: new Date().toISOString(),
});
// 1. 初始化Zulip管理员客户端
this.logger.log(`[${operationId}] 步骤2: 开始初始化Zulip管理员客户端`, {
operation: 'register',
operationId,
step: 'initializeZulipAdminClient',
});
await this.initializeZulipAdminClient();
this.logger.log(`[${operationId}] 步骤2: Zulip管理员客户端初始化成功`, {
operation: 'register',
operationId,
step: 'initializeZulipAdminClient',
result: 'success',
});
// 2. 调用核心服务进行注册
this.logger.log(`[${operationId}] 步骤3: 开始创建游戏用户账号`, {
operation: 'register',
operationId,
step: 'createGameUser',
username: registerRequest.username,
});
const authResult = await this.loginCoreService.register(registerRequest);
this.logger.log(`[${operationId}] 步骤3: 游戏用户账号创建成功`, {
operation: 'register',
operationId,
step: 'createGameUser',
result: 'success',
gameUserId: authResult.user.id.toString(),
username: authResult.user.username,
email: authResult.user.email,
});
// 3. 创建Zulip账号使用相同的邮箱和密码
let zulipAccountCreated = false;
if (registerRequest.email && registerRequest.password) {
this.logger.log(`[${operationId}] 步骤4: 开始创建/绑定Zulip账号`, {
operation: 'register',
operationId,
step: 'createZulipAccount',
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
} else {
this.logger.warn(`[${operationId}] 步骤4: 跳过Zulip账号创建缺少邮箱或密码`, {
operation: 'register',
operationId,
step: 'createZulipAccount',
result: 'skipped',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
hasEmail: !!registerRequest.email,
hasPassword: !!registerRequest.password,
});
}
try {
if (registerRequest.email && registerRequest.password) {
await this.createZulipAccountForUser(authResult.user, registerRequest.password);
zulipAccountCreated = true;
this.logger.log(`Zulip账号创建成功: ${registerRequest.username}`, {
this.logger.log(`[${operationId}] 步骤4: Zulip账号创建成功`, {
operation: 'register',
operationId,
step: 'createZulipAccount',
result: 'success',
gameUserId: authResult.user.id.toString(),
email: registerRequest.email,
});
@@ -247,21 +314,41 @@ export class LoginService {
}
} catch (zulipError) {
const err = zulipError as Error;
this.logger.error(`Zulip账号创建失败回滚用户注册`, {
this.logger.error(`[${operationId}] 步骤4: Zulip账号创建失败开始回滚`, {
operation: 'register',
operationId,
step: 'createZulipAccount',
result: 'failed',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
zulipError: err.message,
}, err.stack);
// 回滚游戏用户注册
this.logger.log(`[${operationId}] 步骤4.1: 开始回滚游戏用户注册`, {
operation: 'register',
operationId,
step: 'rollbackGameUser',
gameUserId: authResult.user.id.toString(),
});
try {
await this.loginCoreService.deleteUser(authResult.user.id);
this.logger.log(`用户注册回滚成功: ${registerRequest.username}`);
this.logger.log(`[${operationId}] 步骤4.1: 游戏用户注册回滚成功`, {
operation: 'register',
operationId,
step: 'rollbackGameUser',
result: 'success',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
});
} catch (rollbackError) {
const rollbackErr = rollbackError as Error;
this.logger.error(`用户注册回滚失败`, {
this.logger.error(`[${operationId}] 步骤4.1: 游戏用户注册回滚失败`, {
operation: 'register',
operationId,
step: 'rollbackGameUser',
result: 'failed',
username: registerRequest.username,
gameUserId: authResult.user.id.toString(),
rollbackError: rollbackErr.message,
@@ -273,9 +360,34 @@ export class LoginService {
}
// 4. 生成JWT令牌对通过Core层
this.logger.log(`[${operationId}] 步骤5: 开始生成JWT令牌`, {
operation: 'register',
operationId,
step: 'generateTokens',
gameUserId: authResult.user.id.toString(),
});
const tokenPair = await this.loginCoreService.generateTokenPair(authResult.user);
this.logger.log(`[${operationId}] 步骤5: JWT令牌生成成功`, {
operation: 'register',
operationId,
step: 'generateTokens',
result: 'success',
gameUserId: authResult.user.id.toString(),
tokenType: tokenPair.token_type,
expiresIn: tokenPair.expires_in,
});
// 5. 格式化响应数据
this.logger.log(`[${operationId}] 步骤6: 格式化响应数据`, {
operation: 'register',
operationId,
step: 'formatResponse',
gameUserId: authResult.user.id.toString(),
zulipAccountCreated,
});
const response: LoginResponse = {
user: this.formatUserInfo(authResult.user),
access_token: tokenPair.access_token,
@@ -288,10 +400,13 @@ export class LoginService {
const duration = Date.now() - startTime;
this.logger.log(`用户注册成功: ${authResult.user.username} (ID: ${authResult.user.id})`, {
this.logger.log(`[${operationId}] 注册流程完成: 用户注册成功`, {
operation: 'register',
operationId,
result: 'success',
gameUserId: authResult.user.id.toString(),
username: authResult.user.username,
email: authResult.user.email,
zulipAccountCreated,
duration,
timestamp: new Date().toISOString(),
@@ -306,9 +421,12 @@ export class LoginService {
const duration = Date.now() - startTime;
const err = error as Error;
this.logger.error(`用户注册失败: ${registerRequest.username}`, {
this.logger.error(`[${operationId}] 注册流程失败: 用户注册失败`, {
operation: 'register',
operationId,
result: 'failed',
username: registerRequest.username,
email: registerRequest.email,
error: err.message,
duration,
timestamp: new Date().toISOString(),

View File

@@ -0,0 +1,443 @@
/**
* 登录服务Zulip集成测试
*
* 功能描述:
* - 测试用户注册时的Zulip账号创建/绑定逻辑
* - 测试用户登录时的Zulip集成处理
* - 验证API Key的获取和存储机制
* - 测试各种异常情况的处理
*
* 测试场景:
* - 注册时Zulip中没有用户创建新账号
* - 注册时Zulip中已有用户绑定已有账号
* - 登录时没有Zulip关联尝试创建/绑定
* - 登录时已有Zulip关联刷新API Key
* - 各种错误情况的处理和回滚
*
* @author moyin
* @version 1.0.0
* @since 2026-01-10
* @lastModified 2026-01-10
*/
import { Test, TestingModule } from '@nestjs/testing';
import { Logger } from '@nestjs/common';
import { LoginService } from './login.service';
import { LoginCoreService } from '../../core/login_core/login_core.service';
import { ZulipAccountService } from '../../core/zulip_core/services/zulip_account.service';
import { ZulipAccountsService } from '../../core/db/zulip_accounts/zulip_accounts.service';
import { ApiKeySecurityService } from '../../core/zulip_core/services/api_key_security.service';
import { Users } from '../../core/db/users/users.entity';
import { ZulipAccountResponseDto } from '../../core/db/zulip_accounts/zulip_accounts.dto';
describe('LoginService - Zulip Integration', () => {
let service: LoginService;
let loginCoreService: jest.Mocked<LoginCoreService>;
let zulipAccountService: jest.Mocked<ZulipAccountService>;
let zulipAccountsService: jest.Mocked<ZulipAccountsService>;
let apiKeySecurityService: jest.Mocked<ApiKeySecurityService>;
const mockUser: Users = {
id: BigInt(12345),
username: 'testuser',
nickname: '测试用户',
email: 'test@example.com',
email_verified: false,
phone: null,
password_hash: 'hashedpassword',
github_id: null,
avatar_url: null,
role: 1,
status: 'active',
created_at: new Date(),
updated_at: new Date(),
} as Users;
beforeEach(async () => {
const mockLoginCoreService = {
register: jest.fn(),
login: jest.fn(),
generateTokenPair: jest.fn(),
};
const mockZulipAccountService = {
createZulipAccount: jest.fn(),
initializeAdminClient: jest.fn(),
};
const mockZulipAccountsService = {
findByGameUserId: jest.fn(),
create: jest.fn(),
updateByGameUserId: jest.fn(),
};
const mockApiKeySecurityService = {
storeApiKey: jest.fn(),
getApiKey: jest.fn(),
};
const module: TestingModule = await Test.createTestingModule({
providers: [
LoginService,
{
provide: LoginCoreService,
useValue: mockLoginCoreService,
},
{
provide: ZulipAccountService,
useValue: mockZulipAccountService,
},
{
provide: 'ZulipAccountsService',
useValue: mockZulipAccountsService,
},
{
provide: ApiKeySecurityService,
useValue: mockApiKeySecurityService,
},
],
}).compile();
service = module.get<LoginService>(LoginService);
loginCoreService = module.get(LoginCoreService);
zulipAccountService = module.get(ZulipAccountService);
zulipAccountsService = module.get('ZulipAccountsService');
apiKeySecurityService = module.get(ApiKeySecurityService);
// 模拟Logger以避免日志输出
jest.spyOn(Logger.prototype, 'log').mockImplementation();
jest.spyOn(Logger.prototype, 'error').mockImplementation();
jest.spyOn(Logger.prototype, 'warn').mockImplementation();
jest.spyOn(Logger.prototype, 'debug').mockImplementation();
});
describe('用户注册时的Zulip集成', () => {
it('应该在Zulip中不存在用户时创建新账号', async () => {
// 准备测试数据
const registerRequest = {
username: 'testuser',
password: 'password123',
nickname: '测试用户',
email: 'test@example.com',
};
const mockAuthResult = {
user: mockUser,
isNewUser: true,
};
const mockTokenPair = {
access_token: 'access_token',
refresh_token: 'refresh_token',
expires_in: 3600,
token_type: 'Bearer',
};
const mockZulipCreateResult = {
success: true,
userId: 67890,
email: 'test@example.com',
apiKey: 'test_api_key_12345678901234567890',
isExistingUser: false,
};
// 设置模拟返回值
loginCoreService.register.mockResolvedValue(mockAuthResult);
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipCreateResult);
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
zulipAccountsService.create.mockResolvedValue({} as any);
// 模拟私有方法
const checkZulipUserExistsSpy = jest.spyOn(service as any, 'checkZulipUserExists')
.mockResolvedValue({ exists: false });
jest.spyOn(service as any, 'initializeZulipAdminClient')
.mockResolvedValue(true);
// 执行测试
const result = await service.register(registerRequest);
// 验证结果
expect(result.success).toBe(true);
expect(result.data?.is_new_user).toBe(true);
expect(result.data?.message).toContain('Zulip');
// 验证调用
expect(loginCoreService.register).toHaveBeenCalledWith(registerRequest);
expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345');
expect(checkZulipUserExistsSpy).toHaveBeenCalledWith('test@example.com');
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
email: 'test@example.com',
fullName: '测试用户',
password: 'password123',
});
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'test_api_key_12345678901234567890');
expect(zulipAccountsService.create).toHaveBeenCalledWith({
gameUserId: '12345',
zulipUserId: 67890,
zulipEmail: 'test@example.com',
zulipFullName: '测试用户',
zulipApiKeyEncrypted: 'stored_in_redis',
status: 'active',
lastVerifiedAt: expect.any(Date),
});
});
it('应该在Zulip中已存在用户时绑定账号', async () => {
// 准备测试数据
const registerRequest = {
username: 'testuser',
password: 'password123',
nickname: '测试用户',
email: 'test@example.com',
};
const mockAuthResult = {
user: mockUser,
isNewUser: true,
};
const mockTokenPair = {
access_token: 'access_token',
refresh_token: 'refresh_token',
expires_in: 3600,
token_type: 'Bearer',
};
// 设置模拟返回值
loginCoreService.register.mockResolvedValue(mockAuthResult);
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
zulipAccountsService.create.mockResolvedValue({} as any);
// 模拟私有方法
jest.spyOn(service as any, 'checkZulipUserExists')
.mockResolvedValue({ exists: true, userId: 67890 });
const refreshZulipApiKeySpy = jest.spyOn(service as any, 'refreshZulipApiKey')
.mockResolvedValue({ success: true, apiKey: 'existing_api_key_12345678901234567890' });
jest.spyOn(service as any, 'initializeZulipAdminClient')
.mockResolvedValue(true);
// 执行测试
const result = await service.register(registerRequest);
// 验证结果
expect(result.success).toBe(true);
expect(result.data?.message).toContain('绑定');
// 验证调用
expect(refreshZulipApiKeySpy).toHaveBeenCalledWith('test@example.com', 'password123');
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'existing_api_key_12345678901234567890');
expect(zulipAccountsService.create).toHaveBeenCalledWith({
gameUserId: '12345',
zulipUserId: 67890,
zulipEmail: 'test@example.com',
zulipFullName: '测试用户',
zulipApiKeyEncrypted: 'stored_in_redis',
status: 'active',
lastVerifiedAt: expect.any(Date),
});
});
});
describe('用户登录时的Zulip集成', () => {
it('应该在用户没有Zulip关联时尝试创建/绑定', async () => {
// 准备测试数据
const loginRequest = {
identifier: 'testuser',
password: 'password123',
};
const mockAuthResult = {
user: mockUser,
isNewUser: false,
};
const mockTokenPair = {
access_token: 'access_token',
refresh_token: 'refresh_token',
expires_in: 3600,
token_type: 'Bearer',
};
const mockZulipCreateResult = {
success: true,
userId: 67890,
email: 'test@example.com',
apiKey: 'new_api_key_12345678901234567890',
isExistingUser: false,
};
// 设置模拟返回值
loginCoreService.login.mockResolvedValue(mockAuthResult);
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
zulipAccountService.createZulipAccount.mockResolvedValue(mockZulipCreateResult);
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
zulipAccountsService.create.mockResolvedValue({} as any);
// 模拟私有方法
jest.spyOn(service as any, 'checkZulipUserExists')
.mockResolvedValue({ exists: false });
jest.spyOn(service as any, 'initializeZulipAdminClient')
.mockResolvedValue(true);
// 执行测试
const result = await service.login(loginRequest);
// 验证结果
expect(result.success).toBe(true);
expect(result.data?.is_new_user).toBe(false);
// 验证调用
expect(loginCoreService.login).toHaveBeenCalledWith(loginRequest);
expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345');
expect(zulipAccountService.createZulipAccount).toHaveBeenCalledWith({
email: 'test@example.com',
fullName: '测试用户',
password: 'password123',
});
});
it('应该在用户已有Zulip关联时刷新API Key', async () => {
// 准备测试数据
const loginRequest = {
identifier: 'testuser',
password: 'password123',
};
const mockAuthResult = {
user: mockUser,
isNewUser: false,
};
const mockTokenPair = {
access_token: 'access_token',
refresh_token: 'refresh_token',
expires_in: 3600,
token_type: 'Bearer',
};
const mockExistingAccount: ZulipAccountResponseDto = {
id: '1',
gameUserId: '12345',
zulipUserId: 67890,
zulipEmail: 'test@example.com',
zulipFullName: '测试用户',
status: 'active' as const,
lastVerifiedAt: new Date().toISOString(),
retryCount: 0,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(),
};
// 设置模拟返回值
loginCoreService.login.mockResolvedValue(mockAuthResult);
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
zulipAccountsService.findByGameUserId.mockResolvedValue(mockExistingAccount);
apiKeySecurityService.storeApiKey.mockResolvedValue({ success: true, message: 'Stored' });
zulipAccountsService.updateByGameUserId.mockResolvedValue({} as any);
// 模拟私有方法
const refreshZulipApiKeySpy = jest.spyOn(service as any, 'refreshZulipApiKey')
.mockResolvedValue({ success: true, apiKey: 'refreshed_api_key_12345678901234567890' });
// 执行测试
const result = await service.login(loginRequest);
// 验证结果
expect(result.success).toBe(true);
// 验证调用
expect(zulipAccountsService.findByGameUserId).toHaveBeenCalledWith('12345');
expect(refreshZulipApiKeySpy).toHaveBeenCalledWith('test@example.com', 'password123');
expect(apiKeySecurityService.storeApiKey).toHaveBeenCalledWith('12345', 'refreshed_api_key_12345678901234567890');
expect(zulipAccountsService.updateByGameUserId).toHaveBeenCalledWith('12345', {
lastVerifiedAt: expect.any(Date),
status: 'active',
errorMessage: null,
});
});
});
describe('错误处理', () => {
it('应该在Zulip创建失败时回滚用户注册', async () => {
// 准备测试数据
const registerRequest = {
username: 'testuser',
password: 'password123',
nickname: '测试用户',
email: 'test@example.com',
};
const mockAuthResult = {
user: mockUser,
isNewUser: true,
};
// 设置模拟返回值
loginCoreService.register.mockResolvedValue(mockAuthResult);
loginCoreService.deleteUser = jest.fn().mockResolvedValue(true);
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
// 模拟Zulip创建失败
jest.spyOn(service as any, 'checkZulipUserExists')
.mockResolvedValue({ exists: false });
jest.spyOn(service as any, 'initializeZulipAdminClient')
.mockResolvedValue(true);
zulipAccountService.createZulipAccount.mockResolvedValue({
success: false,
error: 'Zulip服务器错误',
});
// 执行测试
const result = await service.register(registerRequest);
// 验证结果
expect(result.success).toBe(false);
expect(result.message).toContain('Zulip账号创建失败');
// 验证回滚调用
expect(loginCoreService.deleteUser).toHaveBeenCalledWith(mockUser.id);
});
it('应该在登录时Zulip集成失败但不影响登录', async () => {
// 准备测试数据
const loginRequest = {
identifier: 'testuser',
password: 'password123',
};
const mockAuthResult = {
user: mockUser,
isNewUser: false,
};
const mockTokenPair = {
access_token: 'access_token',
refresh_token: 'refresh_token',
expires_in: 3600,
token_type: 'Bearer',
};
// 设置模拟返回值
loginCoreService.login.mockResolvedValue(mockAuthResult);
loginCoreService.generateTokenPair.mockResolvedValue(mockTokenPair);
zulipAccountsService.findByGameUserId.mockResolvedValue(null);
// 模拟Zulip集成失败
jest.spyOn(service as any, 'initializeZulipAdminClient')
.mockRejectedValue(new Error('Zulip服务器不可用'));
// 执行测试
const result = await service.login(loginRequest);
// 验证结果 - 登录应该成功即使Zulip集成失败
expect(result.success).toBe(true);
expect(result.data?.access_token).toBe('access_token');
});
});
});