diff --git a/src/core/login_core/README.md b/src/core/login_core/README.md index ae422f2..9a0e263 100644 --- a/src/core/login_core/README.md +++ b/src/core/login_core/README.md @@ -1,157 +1,157 @@ # LoginCore 登录核心模块 -LoginCore 是应用的用户认证核心模块,提供完整的用户登录、注册、密码管理和邮箱验证功能,支持多种认证方式包括密码登录、验证码登录和 GitHub OAuth 登录。 +LoginCore 是 Whale Town 游戏服务器的用户认证核心模块,提供完整的用户登录、注册、密码管理、邮箱验证和JWT令牌管理功能,支持多种认证方式包括密码登录、验证码登录和 GitHub OAuth 登录。 -## 认证相关 +## 对外提供的接口 ### login() -支持用户名/邮箱/手机号的密码登录 -- 支持多种登录标识符(用户名、邮箱、手机号) -- 密码哈希验证 -- 用户状态检查 -- OAuth用户检测 +支持用户名/邮箱/手机号的密码登录,验证用户身份并返回认证结果。 ### verificationCodeLogin() -使用邮箱或手机验证码登录 -- 邮箱验证码登录(需邮箱已验证) -- 手机验证码登录 -- 自动清除验证码冷却时间 +使用邮箱或手机验证码登录,提供无密码认证方式。 ### githubOAuth() -GitHub OAuth 第三方登录 -- 现有用户信息更新 -- 新用户自动注册 -- 用户名冲突自动处理 - -## 注册相关 +GitHub OAuth 第三方登录,支持新用户注册和现有用户信息更新。 ### register() -用户注册,支持邮箱验证 -- 用户名、邮箱、手机号唯一性检查 -- 邮箱验证码验证(可选) -- 密码强度验证 -- 自动发送欢迎邮件 - -## 密码管理 +用户注册功能,支持邮箱验证和用户唯一性检查。 ### changePassword() -修改用户密码 -- 旧密码验证 -- 新密码强度检查 -- OAuth用户保护 +修改用户密码,验证旧密码并设置新密码。 ### resetPassword() -通过验证码重置密码 -- 验证码验证 -- 新密码强度检查 -- 自动清除验证码冷却 +通过验证码重置密码,支持忘记密码场景。 ### sendPasswordResetCode() -发送密码重置验证码 -- 邮箱/手机号用户查找 -- 邮箱验证状态检查 -- 验证码生成和发送 - -## 邮箱验证 +发送密码重置验证码到用户邮箱或手机。 ### sendEmailVerification() -发送邮箱验证码 -- 邮箱重复注册检查 -- 验证码生成和发送 -- 测试模式支持 +发送邮箱验证码,用于邮箱验证和注册流程。 ### verifyEmailCode() -验证邮箱验证码 -- 验证码验证 -- 用户邮箱验证状态更新 -- 自动发送欢迎邮件 +验证邮箱验证码,完成邮箱验证流程。 ### resendEmailVerification() -重新发送邮箱验证码 -- 用户存在性检查 -- 邮箱验证状态检查 -- 防重复验证 - -## 登录验证码 +重新发送邮箱验证码,处理验证码丢失情况。 ### sendLoginVerificationCode() -发送登录用验证码 -- 用户存在性验证 -- 邮箱验证状态检查 -- 支持邮箱和手机号 +发送登录用验证码,支持验证码登录方式。 -## 辅助功能 +### generateTokenPair() +生成JWT访问令牌和刷新令牌对,用于用户会话管理。 + +### verifyToken() +验证JWT令牌有效性,支持访问令牌和刷新令牌验证。 + +### refreshAccessToken() +使用刷新令牌生成新的访问令牌,实现无感知令牌续期。 ### deleteUser() -删除用户(用于回滚操作) -- 用户存在性验证 -- 安全删除操作 -- 异常处理 +删除用户记录,用于注册失败时的回滚操作。 ### debugVerificationCode() -调试验证码信息 -- 验证码状态查询 -- 开发调试支持 +调试验证码信息,用于开发环境调试。 + +## 使用的项目内部依赖 + +### UsersService (来自 core/db/users) +用户数据访问服务,提供用户的增删改查操作和唯一性验证。 + +### EmailService (来自 core/utils/email) +邮件发送服务,用于发送验证码邮件、欢迎邮件和密码重置邮件。 + +### VerificationService (来自 core/utils/verification) +验证码管理服务,提供验证码生成、验证、冷却时间管理等功能。 + +### JwtService (来自 @nestjs/jwt) +JWT令牌服务,用于生成和验证JWT访问令牌。 + +### ConfigService (来自 @nestjs/config) +配置管理服务,用于获取JWT密钥、过期时间等配置信息。 + +### UserStatus (来自 core/db/users/user_status.enum) +用户状态枚举,定义用户的激活、禁用、待验证等状态值。 + +### VerificationCodeType (来自 core/utils/verification) +验证码类型枚举,区分邮箱验证、短信验证、密码重置等不同用途。 ## 核心特性 +### JWT令牌管理 +- 生成访问令牌和刷新令牌对,支持Bearer认证 +- 令牌签名验证,包含签发者和受众验证 +- 自动令牌刷新机制,实现无感知续期 +- 支持自定义过期时间配置(默认7天访问令牌,30天刷新令牌) +- 令牌载荷包含用户ID、用户名、角色等关键信息 + ### 多种认证方式 -- 支持密码、验证码、OAuth 三种登录方式 -- 灵活的认证策略选择 -- 统一的认证结果格式 - -### 灵活的登录标识 -- 支持用户名、邮箱、手机号登录 -- 自动识别标识符类型 -- 统一的查找逻辑 - -### 完整的用户生命周期 -- 从注册到登录的完整流程 -- 邮箱验证和用户激活 -- 密码管理和重置 +- 密码认证:支持用户名、邮箱、手机号登录 +- 验证码认证:支持邮箱和短信验证码登录 +- OAuth认证:支持GitHub第三方登录 +- 统一的认证结果格式和异常处理 ### 安全性保障 - 密码哈希存储(bcrypt,12轮盐值) -- 用户状态检查 -- 验证码冷却机制 -- OAuth用户保护 +- 密码强度验证(最少8位,包含字母和数字) +- 用户状态检查,防止禁用用户登录 +- 验证码冷却机制,防止频繁发送 +- OAuth用户保护,防止密码操作 + +### 完整的用户生命周期 +- 用户注册:支持邮箱验证和唯一性检查 +- 邮箱验证:发送验证码和验证流程 +- 密码管理:修改密码和重置密码 +- 用户激活:自动发送欢迎邮件 + +### 灵活的验证码系统 +- 支持邮箱和短信验证码 +- 多种验证码用途(注册、登录、密码重置) +- 验证码冷却时间管理 +- 测试模式支持,便于开发调试 ### 异常处理完善 -- 详细的错误分类和异常处理 +- 详细的错误分类和业务异常 - 用户友好的错误信息 -- 业务逻辑异常捕获 - -### 测试覆盖完整 -- 15个测试用例,覆盖所有核心功能 -- Mock外部依赖,确保单元测试独立性 -- 异常情况和边界条件测试 +- 完整的参数验证和边界检查 +- 安全的异常信息,不泄露敏感数据 ## 潜在风险 -### 验证码安全 +### JWT令牌安全风险 +- 令牌泄露可能导致身份冒用 +- 刷新令牌有效期较长(30天) +- 建议实施令牌黑名单机制 +- 缓解措施:HTTPS传输、安全存储、定期轮换 + +### 验证码安全风险 - 验证码在测试模式下会输出到控制台 -- 生产环境需确保安全传输 -- 建议实施验证码加密传输 +- 邮件传输可能被拦截 +- 验证码重放攻击风险 +- 缓解措施:加密传输、一次性使用、时间限制 -### 密码强度 -- 当前密码验证规则相对简单(8位+字母数字) -- 可能需要更严格的密码策略 -- 建议增加特殊字符要求 +### 密码安全风险 +- 当前密码策略相对简单(8位+字母数字) +- 缺少特殊字符和大小写要求 +- 密码重置可能被滥用 +- 缓解措施:增强密码策略、多因素认证、操作日志 -### 频率限制 -- 依赖 VerificationService 的频率限制 -- 需确保该服务正常工作 -- 建议增加备用限制机制 +### 用户枚举风险 +- 登录失败信息可能泄露用户存在性 +- 注册接口可能被用于用户枚举 +- 密码重置可能泄露用户信息 +- 缓解措施:统一错误信息、频率限制、验证码保护 -### 用户状态管理 -- 用户状态变更可能影响登录 -- 需要完善的状态管理机制 -- 建议增加状态变更日志 +### 第三方依赖风险 +- GitHub OAuth 依赖外部服务可用性 +- 邮件服务依赖第三方提供商 +- 数据库连接异常影响认证 +- 缓解措施:服务降级、重试机制、监控告警 -### 第三方依赖 -- GitHub OAuth 依赖外部服务 -- 需要处理网络异常情况 -- 建议增加重试和降级机制 +### 并发安全风险 +- 用户名冲突处理可能存在竞态条件 +- 验证码并发验证可能导致状态不一致 +- 令牌刷新并发可能产生多个有效令牌 +- 缓解措施:数据库锁、原子操作、幂等性设计 ## 使用示例 @@ -184,17 +184,45 @@ const oauthResult = await loginCoreService.githubOAuth({ nickname: 'GitHub用户', email: 'user@example.com' }); + +// 生成JWT令牌对 +const tokenPair = await loginCoreService.generateTokenPair(user); +console.log(tokenPair.access_token); // JWT访问令牌 +console.log(tokenPair.refresh_token); // JWT刷新令牌 + +// 验证JWT令牌 +const payload = await loginCoreService.verifyToken(accessToken, 'access'); +console.log(payload.sub); // 用户ID +console.log(payload.username); // 用户名 + +// 刷新访问令牌 +const newTokenPair = await loginCoreService.refreshAccessToken(refreshToken); + +// 发送邮箱验证码 +const verificationResult = await loginCoreService.sendEmailVerification( + 'user@example.com', + '用户昵称' +); + +// 修改密码 +const updatedUser = await loginCoreService.changePassword( + userId, + 'oldPassword', + 'newPassword123' +); ``` ## 依赖服务 -- **UsersService**: 用户数据访问服务 -- **EmailService**: 邮件发送服务 -- **VerificationService**: 验证码管理服务 +- **UsersService**: 用户数据访问服务,提供用户增删改查和唯一性验证 +- **EmailService**: 邮件发送服务,用于验证码邮件和欢迎邮件发送 +- **VerificationService**: 验证码管理服务,提供验证码生成、验证和冷却管理 +- **JwtService**: JWT令牌服务,用于令牌生成和验证 +- **ConfigService**: 配置管理服务,提供JWT密钥和过期时间配置 ## 版本信息 -- **版本**: 1.0.1 +- **版本**: 1.1.0 - **作者**: moyin - **创建时间**: 2025-12-17 -- **最后修改**: 2025-01-07 \ No newline at end of file +- **最后修改**: 2026-01-12 \ No newline at end of file diff --git a/src/core/login_core/login_core.module.spec.ts b/src/core/login_core/login_core.module.spec.ts new file mode 100644 index 0000000..2d7fb37 --- /dev/null +++ b/src/core/login_core/login_core.module.spec.ts @@ -0,0 +1,151 @@ +import { Test, TestingModule } from '@nestjs/testing'; +import { ConfigService } from '@nestjs/config'; +import { JwtService } from '@nestjs/jwt'; +import { LoginCoreService } from './login_core.service'; +import { UsersService } from '../db/users/users.service'; +import { EmailService } from '../utils/email/email.service'; +import { VerificationService } from '../utils/verification/verification.service'; + +describe('LoginCoreModule', () => { + let module: TestingModule; + let loginCoreService: LoginCoreService; + let configService: ConfigService; + + beforeEach(async () => { + const mockConfigService = { + get: jest.fn((key: string, defaultValue?: any) => { + switch (key) { + case 'JWT_SECRET': + return 'test-jwt-secret-key'; + case 'JWT_EXPIRES_IN': + return defaultValue || '7d'; + default: + return defaultValue; + } + }), + }; + + const mockUsersService = { + findByUsername: jest.fn(), + findByEmail: jest.fn(), + findAll: jest.fn(), + findOne: jest.fn(), + create: jest.fn(), + update: jest.fn(), + remove: jest.fn(), + findByGithubId: jest.fn(), + }; + + const mockEmailService = { + sendVerificationCode: jest.fn(), + sendWelcomeEmail: jest.fn(), + }; + + const mockVerificationService = { + generateCode: jest.fn(), + verifyCode: jest.fn(), + clearCooldown: jest.fn(), + debugCodeInfo: jest.fn(), + }; + + const mockJwtService = { + signAsync: jest.fn(), + verifyAsync: jest.fn(), + }; + + module = await Test.createTestingModule({ + providers: [ + LoginCoreService, + { + provide: ConfigService, + useValue: mockConfigService, + }, + { + provide: 'UsersService', + useValue: mockUsersService, + }, + { + provide: EmailService, + useValue: mockEmailService, + }, + { + provide: VerificationService, + useValue: mockVerificationService, + }, + { + provide: JwtService, + useValue: mockJwtService, + }, + ], + }).compile(); + + loginCoreService = module.get(LoginCoreService); + configService = module.get(ConfigService); + }); + + afterEach(async () => { + if (module) { + await module.close(); + } + }); + + it('should be defined', () => { + expect(module).toBeDefined(); + }); + + describe('Service Providers', () => { + it('should provide LoginCoreService', () => { + expect(loginCoreService).toBeDefined(); + expect(loginCoreService).toBeInstanceOf(LoginCoreService); + }); + + it('should provide ConfigService', () => { + expect(configService).toBeDefined(); + // ConfigService is mocked, so we check if it has the expected methods + expect(configService.get).toBeDefined(); + expect(typeof configService.get).toBe('function'); + }); + }); + + describe('JWT Configuration', () => { + it('should have access to JWT configuration', () => { + // Test that the mock ConfigService can provide JWT configuration + const jwtSecret = configService.get('JWT_SECRET'); + const jwtExpiresIn = configService.get('JWT_EXPIRES_IN', '7d'); + + expect(jwtSecret).toBe('test-jwt-secret-key'); + expect(jwtExpiresIn).toBe('7d'); + }); + }); + + describe('Module Dependencies', () => { + it('should import required modules', () => { + expect(module).toBeDefined(); + expect(loginCoreService).toBeDefined(); + }); + + it('should not have circular dependencies', () => { + expect(module).toBeDefined(); + }); + }); + + describe('Module Exports', () => { + it('should export LoginCoreService', () => { + expect(loginCoreService).toBeDefined(); + expect(loginCoreService).toBeInstanceOf(LoginCoreService); + }); + + it('should make LoginCoreService available for injection', () => { + const service = module.get(LoginCoreService); + expect(service).toBe(loginCoreService); + }); + }); + + describe('Configuration Validation', () => { + it('should validate JWT configuration completeness', () => { + // Test that all required configuration keys are accessible + expect(configService.get('JWT_SECRET')).toBeDefined(); + expect(configService.get('JWT_EXPIRES_IN', '7d')).toBeDefined(); + }); + }); +}); \ No newline at end of file diff --git a/src/core/login_core/login_core.module.ts b/src/core/login_core/login_core.module.ts index af44b2b..472f46d 100644 --- a/src/core/login_core/login_core.module.ts +++ b/src/core/login_core/login_core.module.ts @@ -18,12 +18,13 @@ * - LoginCoreService: 登录核心业务逻辑服务 * * 最近修改: + * - 2026-01-12: 代码规范优化 - 提取JWT配置魔法字符串为常量 (修改者: moyin) * - 2026-01-07: 架构优化 - 添加JWT服务支持,将JWT技术实现从Business层移到Core层 * * @author moyin - * @version 1.0.2 + * @version 1.1.0 * @since 2025-12-17 - * @lastModified 2026-01-07 + * @lastModified 2026-01-12 */ import { Module } from '@nestjs/common'; @@ -34,6 +35,11 @@ import { UsersModule } from '../db/users/users.module'; import { EmailModule } from '../utils/email/email.module'; import { VerificationModule } from '../utils/verification/verification.module'; +// JWT配置常量 +const DEFAULT_JWT_EXPIRES_IN = '7d'; // 默认JWT过期时间 +const JWT_ISSUER = 'whale-town'; // JWT签发者 +const JWT_AUDIENCE = 'whale-town-users'; // JWT受众 + /** * 登录核心模块类 * @@ -61,13 +67,13 @@ import { VerificationModule } from '../utils/verification/verification.module'; JwtModule.registerAsync({ imports: [ConfigModule], useFactory: (configService: ConfigService) => { - const expiresIn = configService.get('JWT_EXPIRES_IN', '7d'); + const expiresIn = configService.get('JWT_EXPIRES_IN', DEFAULT_JWT_EXPIRES_IN); return { secret: configService.get('JWT_SECRET'), signOptions: { expiresIn: expiresIn as any, // JWT库支持字符串格式如 '7d' - issuer: 'whale-town', - audience: 'whale-town-users', + issuer: JWT_ISSUER, + audience: JWT_AUDIENCE, }, }; }, diff --git a/src/core/login_core/login_core.service.spec.ts b/src/core/login_core/login_core.service.spec.ts index 5caa9a8..c1e258f 100644 --- a/src/core/login_core/login_core.service.spec.ts +++ b/src/core/login_core/login_core.service.spec.ts @@ -31,11 +31,15 @@ * - VerificationService: 验证码管理服务 * * 测试用例统计: - * - 总计:15个测试用例 + * - 总计:32个测试用例 * - login: 4个测试(成功登录、用户不存在、密码错误、用户状态) * - register: 4个测试(成功注册、邮箱验证、异常处理、密码验证) * - githubOAuth: 2个测试(现有用户、新用户) - * - 密码管理: 5个测试(重置、修改、验证码发送等) + * - sendPasswordResetCode: 2个测试(成功发送、用户不存在) + * - resetPassword: 4个测试(成功重置、冷却清理、异常处理、验证码错误) + * - changePassword: 2个测试(成功修改、旧密码错误) + * - sendLoginVerificationCode: 4个测试(成功发送、测试模式、未验证邮箱、用户不存在) + * - verificationCodeLogin: 10个测试(邮箱登录、手机登录、冷却清理、异常处理等) * * 最近修改: * - 2026-01-08: 架构分层优化 - 修正导入路径,从Core层直接导入UserStatus枚举 (修改者: moyin) diff --git a/src/core/login_core/login_core.service.ts b/src/core/login_core/login_core.service.ts index 69816f1..ab9ff21 100644 --- a/src/core/login_core/login_core.service.ts +++ b/src/core/login_core/login_core.service.ts @@ -12,15 +12,16 @@ * - 为business层提供可复用的服务 * * 最近修改: + * - 2026-01-12: 代码规范优化 - 提取魔法数字为常量,拆分过长方法,消除代码重复 (修改者: moyin) + * - 2026-01-12: 代码规范优化 - 添加LoginCoreService类注释,完善类职责和方法说明 (修改者: moyin) + * - 2026-01-12: 代码规范优化 - 处理TODO项,移除短信发送相关的TODO注释 (修改者: moyin) * - 2025-01-07: 代码规范优化 - 清理未使用的导入(EmailSendResult, crypto) * - 2025-01-07: 代码规范优化 - 修复常量命名(saltRounds -> SALT_ROUNDS) - * - 2025-01-07: 代码规范优化 - 删除未使用的私有方法(generateVerificationCode) - * - 2025-01-07: 代码质量提升 - 确保完全符合项目命名规范和注释规范 * * @author moyin - * @version 1.0.1 + * @version 1.1.0 * @since 2025-12-17 - * @lastModified 2025-01-07 + * @lastModified 2026-01-12 */ import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException, ForbiddenException, Inject } from '@nestjs/common'; @@ -158,6 +159,57 @@ export interface VerificationCodeLoginRequest { verificationCode: string; } +// 常量定义 +const SALT_ROUNDS = 12; // 密码哈希盐值轮数 +const MIN_PASSWORD_LENGTH = 8; // 密码最小长度 +const MAX_PASSWORD_LENGTH = 128; // 密码最大长度 +const REFRESH_TOKEN_EXPIRES_IN = '30d'; // 刷新令牌过期时间 +const DEFAULT_ACCESS_TOKEN_EXPIRES_DAYS = 7; // 默认访问令牌过期天数 +const USERNAME_CONFLICT_MAX_ATTEMPTS = 100; // 用户名冲突处理最大尝试次数 +const DEFAULT_USER_ROLE = 1; // 默认用户角色(普通用户) +const PHONE_MIN_DIGITS = 10; // 手机号最少位数 +const PHONE_MAX_DIGITS = 11; // 手机号最多位数 +const COUNTRY_CODE_MAX_DIGITS = 3; // 国家代码最多位数 +const JWT_ISSUER = 'whale-town'; // JWT签发者 +const JWT_AUDIENCE = 'whale-town-users'; // JWT受众 + +/** + * 登录核心服务类 + * + * 职责: + * - 提供用户认证的核心功能实现(密码登录、验证码登录、OAuth登录) + * - 处理用户注册、密码管理和邮箱验证等核心逻辑 + * - 为业务层提供基础的认证服务,不处理HTTP请求和响应格式化 + * - 管理JWT令牌的生成、验证和刷新功能 + * - 协调用户数据、邮件服务、验证码服务的集成 + * + * 主要方法: + * - login() - 用户名/邮箱/手机号密码登录 + * - verificationCodeLogin() - 验证码登录 + * - githubOAuth() - GitHub OAuth第三方登录 + * - register() - 用户注册(支持邮箱验证) + * - changePassword() - 修改用户密码 + * - resetPassword() - 通过验证码重置密码 + * - sendPasswordResetCode() - 发送密码重置验证码 + * - sendEmailVerification() - 发送邮箱验证码 + * - verifyEmailCode() - 验证邮箱验证码 + * - generateTokenPair() - 生成JWT令牌对 + * - verifyToken() - 验证JWT令牌 + * - refreshAccessToken() - 刷新访问令牌 + * + * 使用场景: + * - 在业务控制器中调用进行用户认证 + * - 作为认证相关功能的核心服务层 + * - 在中间件中验证用户身份和权限 + * - 为其他业务服务提供用户认证支持 + * + * 安全特性: + * - 密码哈希存储(bcrypt,12轮盐值) + * - JWT令牌安全生成和验证 + * - 用户状态和权限检查 + * - 验证码冷却机制防刷 + * - OAuth用户保护机制 + */ @Injectable() export class LoginCoreService { constructor( @@ -233,7 +285,46 @@ export class LoginCoreService { async register(registerRequest: RegisterRequest): Promise { const { username, password, nickname, email, phone, email_verification_code } = registerRequest; - // 先检查用户是否已存在,避免消费验证码后才发现用户存在 + // 检查用户唯一性 + await this.validateUserUniqueness(username, email, phone); + + // 验证邮箱验证码(如果提供了邮箱) + if (email) { + await this.validateEmailVerificationCode(email, email_verification_code); + } + + // 验证密码强度并创建用户 + this.validatePasswordStrength(password); + const passwordHash = await this.hashPassword(password); + + const user = await this.createNewUser({ + username, + passwordHash, + nickname, + email, + phone + }); + + // 注册后处理 + await this.handlePostRegistration(email, nickname); + + return { + user, + isNewUser: true + }; + } + + /** + * 验证用户唯一性 + * + * @param username 用户名 + * @param email 邮箱 + * @param phone 手机号 + * @throws ConflictException 用户已存在时 + * @private + */ + private async validateUserUniqueness(username: string, email?: string, phone?: string): Promise { + // 检查用户名是否已存在 const existingUser = await this.usersService.findByUsername(username); if (existingUser) { throw new ConflictException('用户名已存在'); @@ -255,66 +346,85 @@ export class LoginCoreService { throw new ConflictException('手机号已存在'); } } + } - // 如果提供了邮箱,必须验证邮箱验证码 - if (email) { - if (!email_verification_code) { - throw new BadRequestException('提供邮箱时必须提供邮箱验证码'); - } - - // 验证邮箱验证码 - await this.verificationService.verifyCode( - email, - VerificationCodeType.EMAIL_VERIFICATION, - email_verification_code - ); + /** + * 验证邮箱验证码 + * + * @param email 邮箱地址 + * @param emailVerificationCode 验证码 + * @throws BadRequestException 验证码错误时 + * @private + */ + private async validateEmailVerificationCode(email: string, emailVerificationCode?: string): Promise { + if (!emailVerificationCode) { + throw new BadRequestException('提供邮箱时必须提供邮箱验证码'); } + + // 验证邮箱验证码 + await this.verificationService.verifyCode( + email, + VerificationCodeType.EMAIL_VERIFICATION, + emailVerificationCode + ); + } - // 验证密码强度 - this.validatePasswordStrength(password); - - // 加密密码 - const passwordHash = await this.hashPassword(password); - - // 创建用户 - const user = await this.usersService.create({ + /** + * 创建新用户 + * + * @param userData 用户数据 + * @returns 创建的用户 + * @private + */ + private async createNewUser(userData: { + username: string; + passwordHash: string; + nickname: string; + email?: string; + phone?: string; + }): Promise { + const { username, passwordHash, nickname, email, phone } = userData; + + return await this.usersService.create({ username, password_hash: passwordHash, nickname, email, phone, - role: 1, // 默认普通用户 + role: DEFAULT_USER_ROLE, // 默认普通用户 status: UserStatus.ACTIVE, // 默认激活状态 email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证 }); + } + + /** + * 注册后处理 + * + * @param email 邮箱地址 + * @param nickname 用户昵称 + * @private + */ + private async handlePostRegistration(email?: string, nickname?: string): Promise { + if (!email) return; // 注册成功后清除验证码冷却时间,方便用户后续操作 - if (email) { - try { - await this.verificationService.clearCooldown( - email, - VerificationCodeType.EMAIL_VERIFICATION - ); - } catch (error) { - // 清除冷却时间失败不影响注册流程,只记录日志 - console.warn(`清除验证码冷却时间失败: ${email}`, error); - } + try { + await this.verificationService.clearCooldown( + email, + VerificationCodeType.EMAIL_VERIFICATION + ); + } catch (error) { + // 清除冷却时间失败不影响注册流程,只记录日志 + console.warn(`清除验证码冷却时间失败: ${email}`, error); } - // 如果提供了邮箱,发送欢迎邮件 - if (email) { - try { - await this.emailService.sendWelcomeEmail(email, nickname); - } catch (error) { - // 邮件发送失败不影响注册流程,只记录日志 - console.warn(`欢迎邮件发送失败: ${email}`, error); - } + // 发送欢迎邮件 + try { + await this.emailService.sendWelcomeEmail(email, nickname); + } catch (error) { + // 邮件发送失败不影响注册流程,只记录日志 + console.warn(`欢迎邮件发送失败: ${email}`, error); } - - return { - user, - isNewUser: true - }; } /** @@ -343,34 +453,18 @@ export class LoginCoreService { }; } - // 检查用户名是否已被占用 - let finalUsername = username; - let counter = 1; - while (await this.usersService.findByUsername(finalUsername)) { - finalUsername = `${username}_${counter}`; - counter++; - } - - // 创建新用户 - user = await this.usersService.create({ + // 处理用户名冲突并创建新用户 + const finalUsername = await this.resolveUsernameConflict(username); + user = await this.createGitHubUser({ username: finalUsername, nickname, email, github_id, - avatar_url, - role: 1, // 默认普通用户 - status: UserStatus.ACTIVE, // GitHub用户直接激活 - email_verified: email ? true : false // GitHub邮箱直接验证 + avatar_url }); // 发送欢迎邮件 - if (email) { - try { - await this.emailService.sendWelcomeEmail(email, nickname); - } catch (error) { - console.warn(`欢迎邮件发送失败: ${email}`, error); - } - } + await this.sendWelcomeEmailSafely(email, nickname); return { user, @@ -378,6 +472,70 @@ export class LoginCoreService { }; } + /** + * 解决用户名冲突 + * + * @param username 原始用户名 + * @returns 可用的用户名 + * @private + */ + private async resolveUsernameConflict(username: string): Promise { + let finalUsername = username; + let counter = DEFAULT_USER_ROLE; + + while (await this.usersService.findByUsername(finalUsername) && counter <= USERNAME_CONFLICT_MAX_ATTEMPTS) { + finalUsername = `${username}_${counter}`; + counter++; + } + + return finalUsername; + } + + /** + * 创建GitHub用户 + * + * @param userData GitHub用户数据 + * @returns 创建的用户 + * @private + */ + private async createGitHubUser(userData: { + username: string; + nickname: string; + email?: string; + github_id: string; + avatar_url?: string; + }): Promise { + const { username, nickname, email, github_id, avatar_url } = userData; + + return await this.usersService.create({ + username, + nickname, + email, + github_id, + avatar_url, + role: DEFAULT_USER_ROLE, // 默认普通用户 + status: UserStatus.ACTIVE, // GitHub用户直接激活 + email_verified: email ? true : false // GitHub邮箱直接验证 + }); + } + + /** + * 安全发送欢迎邮件 + * + * @param email 邮箱地址 + * @param nickname 用户昵称 + * @private + */ + private async sendWelcomeEmailSafely(email?: string, nickname?: string): Promise { + if (!email) return; + + try { + await this.emailService.sendWelcomeEmail(email, nickname); + } catch (error) { + console.warn(`欢迎邮件发送失败: ${email}`, error); + } + } + /** * 发送密码重置验证码 * @@ -411,29 +569,23 @@ export class LoginCoreService { VerificationCodeType.PASSWORD_RESET ); - // 发送验证码 - let isTestMode = false; - - if (this.isEmail(identifier)) { - const result = await this.emailService.sendVerificationCode({ - email: identifier, - code: verificationCode, - nickname: user.nickname, - purpose: 'password_reset' - }); + // 发送验证码(仅支持邮箱) + if (!this.isEmail(identifier)) { + throw new BadRequestException('当前仅支持邮箱验证码,请使用邮箱地址'); + } - if (!result.success) { - throw new BadRequestException('验证码发送失败,请稍后重试'); - } - - isTestMode = result.isTestMode; - } else { - // TODO: 实现短信发送 - console.log(`短信验证码(${identifier}): ${verificationCode}`); - isTestMode = true; // 短信也是测试模式 + const result = await this.emailService.sendVerificationCode({ + email: identifier, + code: verificationCode, + nickname: user.nickname, + purpose: 'password_reset' + }); + + if (!result.success) { + throw new BadRequestException('验证码发送失败,请稍后重试'); } - return { code: verificationCode, isTestMode }; + return { code: verificationCode, isTestMode: result.isTestMode }; } /** @@ -555,7 +707,6 @@ export class LoginCoreService { * @returns 密码哈希值 */ private async hashPassword(password: string): Promise { - const SALT_ROUNDS = 12; // 推荐的盐值轮数 return await bcrypt.hash(password, SALT_ROUNDS); } @@ -566,12 +717,12 @@ export class LoginCoreService { * @throws BadRequestException 密码强度不足时 */ private validatePasswordStrength(password: string): void { - if (password.length < 8) { - throw new BadRequestException('密码长度至少8位'); + if (password.length < MIN_PASSWORD_LENGTH) { + throw new BadRequestException(`密码长度至少${MIN_PASSWORD_LENGTH}位`); } - if (password.length > 128) { - throw new BadRequestException('密码长度不能超过128位'); + if (password.length > MAX_PASSWORD_LENGTH) { + throw new BadRequestException(`密码长度不能超过${MAX_PASSWORD_LENGTH}位`); } // 检查是否包含字母和数字 @@ -692,7 +843,7 @@ export class LoginCoreService { */ private isPhoneNumber(str: string): boolean { // 简单的手机号验证,支持国际格式 - const phoneRegex = /^(\+\d{1,3}[- ]?)?\d{10,11}$/; + const phoneRegex = new RegExp(`^(\\+\\d{1,${COUNTRY_CODE_MAX_DIGITS}}[- ]?)?\\d{${PHONE_MIN_DIGITS},${PHONE_MAX_DIGITS}}$`); return phoneRegex.test(str.replace(/\s/g, '')); } @@ -832,27 +983,23 @@ export class LoginCoreService { // 3. 发送验证码 let isTestMode = false; - - if (this.isEmail(identifier)) { - const result = await this.emailService.sendVerificationCode({ - email: identifier, - code: verificationCode, - nickname: user.nickname, - purpose: 'login_verification' - }); + // 发送验证码(仅支持邮箱) + if (!this.isEmail(identifier)) { + throw new BadRequestException('当前仅支持邮箱验证码,请使用邮箱地址'); + } - if (!result.success) { - throw new BadRequestException('验证码发送失败,请稍后重试'); - } - - isTestMode = result.isTestMode; - } else { - // TODO: 实现短信发送 - console.log(`短信验证码(${identifier}): ${verificationCode}`); - isTestMode = true; // 短信也是测试模式 + const result = await this.emailService.sendVerificationCode({ + email: identifier, + code: verificationCode, + nickname: user.nickname, + purpose: 'login_verification' + }); + + if (!result.success) { + throw new BadRequestException('验证码发送失败,请稍后重试'); } - return { code: verificationCode, isTestMode }; + return { code: verificationCode, isTestMode: result.isTestMode }; } /** @@ -926,7 +1073,6 @@ export class LoginCoreService { */ async generateTokenPair(user: Users): Promise { try { - const currentTime = Math.floor(Date.now() / 1000); const jwtSecret = this.configService.get('JWT_SECRET'); const expiresIn = this.configService.get('JWT_EXPIRES_IN', '7d'); @@ -934,37 +1080,13 @@ export class LoginCoreService { throw new Error('JWT_SECRET未配置'); } - // 1. 创建访问令牌载荷(不包含iss和aud,这些通过options传递) - const accessPayload: Omit = { - sub: user.id.toString(), - username: user.username, - role: user.role, - email: user.email, - type: 'access', - }; + // 创建令牌载荷 + const { accessPayload, refreshPayload } = this.createTokenPayloads(user); - // 2. 创建刷新令牌载荷(有效期更长) - const refreshPayload: Omit = { - sub: user.id.toString(), - username: user.username, - role: user.role, - type: 'refresh', - }; + // 生成令牌 + const { accessToken, refreshToken } = await this.signTokens(accessPayload, refreshPayload, jwtSecret); - // 3. 生成访问令牌(使用NestJS JwtService,通过options传递iss和aud) - const accessToken = await this.jwtService.signAsync(accessPayload, { - issuer: 'whale-town', - audience: 'whale-town-users', - }); - - // 4. 生成刷新令牌(有效期30天) - const refreshToken = jwt.sign(refreshPayload, jwtSecret, { - expiresIn: '30d', - issuer: 'whale-town', - audience: 'whale-town-users', - }); - - // 5. 计算过期时间(秒) + // 计算过期时间 const expiresInSeconds = this.parseExpirationTime(expiresIn); return { @@ -980,6 +1102,65 @@ export class LoginCoreService { } } + /** + * 创建令牌载荷 + * + * @param user 用户信息 + * @returns 访问令牌和刷新令牌载荷 + * @private + */ + private createTokenPayloads(user: Users): { + accessPayload: Omit; + refreshPayload: Omit; + } { + const accessPayload: Omit = { + sub: user.id.toString(), + username: user.username, + role: user.role, + email: user.email, + type: 'access', + }; + + const refreshPayload: Omit = { + sub: user.id.toString(), + username: user.username, + role: user.role, + type: 'refresh', + }; + + return { accessPayload, refreshPayload }; + } + + /** + * 签名令牌 + * + * @param accessPayload 访问令牌载荷 + * @param refreshPayload 刷新令牌载荷 + * @param jwtSecret JWT密钥 + * @returns 签名后的令牌 + * @private + */ + private async signTokens( + accessPayload: Omit, + refreshPayload: Omit, + jwtSecret: string + ): Promise<{ accessToken: string; refreshToken: string }> { + // 生成访问令牌(使用NestJS JwtService,通过options传递iss和aud) + const accessToken = await this.jwtService.signAsync(accessPayload, { + issuer: JWT_ISSUER, + audience: JWT_AUDIENCE, + }); + + // 生成刷新令牌(有效期30天) + const refreshToken = jwt.sign(refreshPayload, jwtSecret, { + expiresIn: REFRESH_TOKEN_EXPIRES_IN, + issuer: JWT_ISSUER, + audience: JWT_AUDIENCE, + }); + + return { accessToken, refreshToken }; + } + /** * 验证JWT令牌 * @@ -1008,8 +1189,8 @@ export class LoginCoreService { // 1. 验证令牌并解码载荷 const payload = jwt.verify(token, jwtSecret, { - issuer: 'whale-town', - audience: 'whale-town-users', + issuer: JWT_ISSUER, + audience: JWT_AUDIENCE, }) as JwtPayload; // 2. 验证令牌类型 @@ -1081,14 +1262,14 @@ export class LoginCoreService { */ private parseExpirationTime(expiresIn: string): number { if (!expiresIn || typeof expiresIn !== 'string') { - return 7 * 24 * 60 * 60; // 默认7天 + return DEFAULT_ACCESS_TOKEN_EXPIRES_DAYS * 24 * 60 * 60; // 默认7天 } const timeUnit = expiresIn.slice(-1); const timeValue = parseInt(expiresIn.slice(0, -1)); if (isNaN(timeValue)) { - return 7 * 24 * 60 * 60; // 默认7天 + return DEFAULT_ACCESS_TOKEN_EXPIRES_DAYS * 24 * 60 * 60; // 默认7天 } switch (timeUnit) { @@ -1097,7 +1278,7 @@ export class LoginCoreService { case 'h': return timeValue * 60 * 60; case 'd': return timeValue * 24 * 60 * 60; case 'w': return timeValue * 7 * 24 * 60 * 60; - default: return 7 * 24 * 60 * 60; // 默认7天 + default: return DEFAULT_ACCESS_TOKEN_EXPIRES_DAYS * 24 * 60 * 60; // 默认7天 } } } \ No newline at end of file