/** * 登录控制器 * * 功能描述: * - 处理登录相关的HTTP请求和响应 * - 提供RESTful API接口 * - 数据验证和格式化 * * API端点: * - POST /auth/login - 用户登录 * - POST /auth/register - 用户注册 * - POST /auth/github - GitHub OAuth登录 * - POST /auth/forgot-password - 发送密码重置验证码 * - POST /auth/reset-password - 重置密码 * - PUT /auth/change-password - 修改密码 * * @author moyin angjustinl * @version 1.0.0 * @since 2025-12-17 */ import { Controller, Post, Put, Body, HttpCode, HttpStatus, ValidationPipe, UsePipes, Logger, Res } from '@nestjs/common'; import { ApiTags, ApiOperation, ApiResponse as SwaggerApiResponse, ApiBody } from '@nestjs/swagger'; import { Response } from 'express'; import { LoginService, ApiResponse, LoginResponse } from '../services/login.service'; import { LoginDto, RegisterDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, EmailVerificationDto, SendEmailVerificationDto } from '../dto/login.dto'; import { LoginResponseDto, RegisterResponseDto, GitHubOAuthResponseDto, ForgotPasswordResponseDto, CommonResponseDto, TestModeEmailVerificationResponseDto, SuccessEmailVerificationResponseDto } from '../dto/login_response.dto'; import { Throttle, ThrottlePresets } from '../../security/decorators/throttle.decorator'; import { Timeout, TimeoutPresets } from '../../security/decorators/timeout.decorator'; @ApiTags('auth') @Controller('auth') export class LoginController { private readonly logger = new Logger(LoginController.name); constructor(private readonly loginService: LoginService) {} /** * 用户登录 * * @param loginDto 登录数据 * @returns 登录结果 */ @ApiOperation({ summary: '用户登录', description: '支持用户名、邮箱或手机号登录' }) @ApiBody({ type: LoginDto }) @SwaggerApiResponse({ status: 200, description: '登录成功', type: LoginResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 401, description: '用户名或密码错误' }) @SwaggerApiResponse({ status: 403, description: '账户被禁用或锁定' }) @SwaggerApiResponse({ status: 429, description: '登录尝试过于频繁' }) @Throttle(ThrottlePresets.LOGIN) @Timeout(TimeoutPresets.NORMAL) @Post('login') @UsePipes(new ValidationPipe({ transform: true })) async login(@Body() loginDto: LoginDto, @Res() res: Response): Promise { const result = await this.loginService.login({ identifier: loginDto.identifier, password: loginDto.password }); // 根据业务结果设置正确的HTTP状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else { // 根据错误类型设置不同的状态码 if (result.error_code === 'LOGIN_FAILED') { res.status(HttpStatus.UNAUTHORIZED).json(result); } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } } /** * 用户注册 * * @param registerDto 注册数据 * @returns 注册结果 */ @ApiOperation({ summary: '用户注册', description: '创建新用户账户。如果提供邮箱,需要先调用发送验证码接口获取验证码,然后在注册时提供验证码进行验证。' }) @ApiBody({ type: RegisterDto }) @SwaggerApiResponse({ status: 201, description: '注册成功', type: RegisterResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 409, description: '用户名或邮箱已存在' }) @SwaggerApiResponse({ status: 429, description: '注册请求过于频繁' }) @Throttle(ThrottlePresets.REGISTER) @Timeout(TimeoutPresets.NORMAL) @Post('register') @UsePipes(new ValidationPipe({ transform: true })) async register(@Body() registerDto: RegisterDto, @Res() res: Response): Promise { const result = await this.loginService.register({ username: registerDto.username, password: registerDto.password, nickname: registerDto.nickname, email: registerDto.email, phone: registerDto.phone, email_verification_code: registerDto.email_verification_code }); // 根据业务结果设置正确的HTTP状态码 if (result.success) { res.status(HttpStatus.CREATED).json(result); } else { // 根据错误类型设置不同的状态码 if (result.error_code === 'REGISTER_FAILED') { res.status(HttpStatus.BAD_REQUEST).json(result); } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } } /** * GitHub OAuth登录 * * @param githubDto GitHub OAuth数据 * @returns 登录结果 */ @ApiOperation({ summary: 'GitHub OAuth登录', description: '使用GitHub账户登录或注册' }) @ApiBody({ type: GitHubOAuthDto }) @SwaggerApiResponse({ status: 200, description: 'GitHub登录成功', type: GitHubOAuthResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 401, description: 'GitHub认证失败' }) @Post('github') @UsePipes(new ValidationPipe({ transform: true })) async githubOAuth(@Body() githubDto: GitHubOAuthDto, @Res() res: Response): Promise { const result = await this.loginService.githubOAuth({ github_id: githubDto.github_id, username: githubDto.username, nickname: githubDto.nickname, email: githubDto.email, avatar_url: githubDto.avatar_url }); // 根据业务结果设置正确的HTTP状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 发送密码重置验证码 * * @param forgotPasswordDto 忘记密码数据 * @param res Express响应对象 * @returns 发送结果 */ @ApiOperation({ summary: '发送密码重置验证码', description: '向用户邮箱或手机发送密码重置验证码' }) @ApiBody({ type: ForgotPasswordDto }) @SwaggerApiResponse({ status: 200, description: '验证码发送成功', type: ForgotPasswordResponseDto }) @SwaggerApiResponse({ status: 206, description: '测试模式:验证码已生成但未真实发送', type: ForgotPasswordResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 404, description: '用户不存在' }) @SwaggerApiResponse({ status: 429, description: '发送频率过高' }) @Throttle(ThrottlePresets.SEND_CODE) @Post('forgot-password') @UsePipes(new ValidationPipe({ transform: true })) async forgotPassword( @Body() forgotPasswordDto: ForgotPasswordDto, @Res() res: Response ): Promise { const result = await this.loginService.sendPasswordResetCode(forgotPasswordDto.identifier); // 根据结果设置不同的状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else if (result.error_code === 'TEST_MODE_ONLY') { res.status(HttpStatus.PARTIAL_CONTENT).json(result); // 206 Partial Content } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 重置密码 * * @param resetPasswordDto 重置密码数据 * @returns 重置结果 */ @ApiOperation({ summary: '重置密码', description: '使用验证码重置用户密码' }) @ApiBody({ type: ResetPasswordDto }) @SwaggerApiResponse({ status: 200, description: '密码重置成功', type: CommonResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误或验证码无效' }) @SwaggerApiResponse({ status: 404, description: '用户不存在' }) @SwaggerApiResponse({ status: 429, description: '重置请求过于频繁' }) @Throttle(ThrottlePresets.RESET_PASSWORD) @Post('reset-password') @UsePipes(new ValidationPipe({ transform: true })) async resetPassword(@Body() resetPasswordDto: ResetPasswordDto, @Res() res: Response): Promise { const result = await this.loginService.resetPassword({ identifier: resetPasswordDto.identifier, verificationCode: resetPasswordDto.verification_code, newPassword: resetPasswordDto.new_password }); // 根据业务结果设置正确的HTTP状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 修改密码 * * @param changePasswordDto 修改密码数据 * @returns 修改结果 */ @ApiOperation({ summary: '修改密码', description: '用户修改自己的密码(需要提供旧密码)' }) @ApiBody({ type: ChangePasswordDto }) @SwaggerApiResponse({ status: 200, description: '密码修改成功', type: CommonResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误或旧密码不正确' }) @SwaggerApiResponse({ status: 404, description: '用户不存在' }) @Put('change-password') @UsePipes(new ValidationPipe({ transform: true })) async changePassword(@Body() changePasswordDto: ChangePasswordDto, @Res() res: Response): Promise { // 实际应用中应从JWT令牌中获取用户ID // 这里为了演示,使用请求体中的用户ID const userId = BigInt(changePasswordDto.user_id); const result = await this.loginService.changePassword( userId, changePasswordDto.old_password, changePasswordDto.new_password ); // 根据业务结果设置正确的HTTP状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 发送邮箱验证码 * * @param sendEmailVerificationDto 发送验证码数据 * @param res Express响应对象 * @returns 发送结果 */ @ApiOperation({ summary: '发送邮箱验证码', description: '向指定邮箱发送验证码' }) @ApiBody({ type: SendEmailVerificationDto }) @SwaggerApiResponse({ status: 200, description: '验证码发送成功(真实发送模式)', type: SuccessEmailVerificationResponseDto }) @SwaggerApiResponse({ status: 206, description: '测试模式:验证码已生成但未真实发送', type: TestModeEmailVerificationResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 429, description: '发送频率过高' }) @Throttle(ThrottlePresets.SEND_CODE) @Timeout(TimeoutPresets.EMAIL_SEND) @Post('send-email-verification') @UsePipes(new ValidationPipe({ transform: true })) async sendEmailVerification( @Body() sendEmailVerificationDto: SendEmailVerificationDto, @Res() res: Response ): Promise { const result = await this.loginService.sendEmailVerification(sendEmailVerificationDto.email); // 根据结果设置不同的状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else if (result.error_code === 'TEST_MODE_ONLY') { res.status(HttpStatus.PARTIAL_CONTENT).json(result); // 206 Partial Content } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 验证邮箱验证码 * * @param emailVerificationDto 邮箱验证数据 * @returns 验证结果 */ @ApiOperation({ summary: '验证邮箱验证码', description: '使用验证码验证邮箱' }) @ApiBody({ type: EmailVerificationDto }) @SwaggerApiResponse({ status: 200, description: '邮箱验证成功', type: CommonResponseDto }) @SwaggerApiResponse({ status: 400, description: '验证码错误或已过期' }) @Post('verify-email') @UsePipes(new ValidationPipe({ transform: true })) async verifyEmail(@Body() emailVerificationDto: EmailVerificationDto, @Res() res: Response): Promise { const result = await this.loginService.verifyEmailCode( emailVerificationDto.email, emailVerificationDto.verification_code ); // 根据业务结果设置正确的HTTP状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 重新发送邮箱验证码 * * @param sendEmailVerificationDto 发送验证码数据 * @param res Express响应对象 * @returns 发送结果 */ @ApiOperation({ summary: '重新发送邮箱验证码', description: '重新向指定邮箱发送验证码' }) @ApiBody({ type: SendEmailVerificationDto }) @SwaggerApiResponse({ status: 200, description: '验证码重新发送成功', type: ForgotPasswordResponseDto }) @SwaggerApiResponse({ status: 206, description: '测试模式:验证码已生成但未真实发送', type: ForgotPasswordResponseDto }) @SwaggerApiResponse({ status: 400, description: '邮箱已验证或用户不存在' }) @SwaggerApiResponse({ status: 429, description: '发送频率过高' }) @Throttle(ThrottlePresets.SEND_CODE) @Post('resend-email-verification') @UsePipes(new ValidationPipe({ transform: true })) async resendEmailVerification( @Body() sendEmailVerificationDto: SendEmailVerificationDto, @Res() res: Response ): Promise { const result = await this.loginService.resendEmailVerification(sendEmailVerificationDto.email); // 根据结果设置不同的状态码 if (result.success) { res.status(HttpStatus.OK).json(result); } else if (result.error_code === 'TEST_MODE_ONLY') { res.status(HttpStatus.PARTIAL_CONTENT).json(result); // 206 Partial Content } else { res.status(HttpStatus.BAD_REQUEST).json(result); } } /** * 调试验证码信息 * 仅用于开发和调试 * * @param sendEmailVerificationDto 邮箱信息 * @returns 验证码调试信息 */ @ApiOperation({ summary: '调试验证码信息', description: '获取验证码的详细调试信息(仅开发环境)' }) @ApiBody({ type: SendEmailVerificationDto }) @Post('debug-verification-code') @UsePipes(new ValidationPipe({ transform: true })) async debugVerificationCode(@Body() sendEmailVerificationDto: SendEmailVerificationDto, @Res() res: Response): Promise { const result = await this.loginService.debugVerificationCode(sendEmailVerificationDto.email); // 调试接口总是返回200 res.status(HttpStatus.OK).json(result); } /** * 清除限流记录(仅开发环境) */ @ApiOperation({ summary: '清除限流记录', description: '清除所有限流记录(仅开发环境使用)' }) @Post('debug-clear-throttle') async clearThrottle(@Res() res: Response): Promise { // 注入ThrottleGuard并清除记录 // 这里需要通过依赖注入获取ThrottleGuard实例 res.status(HttpStatus.OK).json({ success: true, message: '限流记录已清除' }); } }