/** * 登录网关控制器 * * 架构层级:Gateway Layer(网关层) * * 功能描述: * - 处理登录相关的HTTP请求和响应 * - 提供RESTful API接口 * - 数据验证和格式化 * - 协议处理和错误响应 * * 职责分离: * - 专注于HTTP协议处理和请求响应 * - 调用业务层服务完成具体功能 * - 处理API文档和参数验证 * - 不包含业务逻辑,只做数据转换和路由 * * 依赖关系: * - 依赖 Business Layer 的 LoginService * - 使用 DTO 进行数据验证 * - 使用 Guard 进行认证保护 * * API端点: * - POST /auth/login - 用户登录 * - POST /auth/github - GitHub OAuth登录 * - POST /auth/forgot-password - 发送密码重置验证码 * - POST /auth/reset-password - 重置密码 * - PUT /auth/change-password - 修改密码 * - POST /auth/refresh-token - 刷新访问令牌 * - POST /auth/verification-code-login - 验证码登录 * - POST /auth/send-login-verification-code - 发送登录验证码 * * @author moyin * @version 2.0.0 * @since 2026-01-14 * @lastModified 2026-01-14 */ 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 } from '../../business/auth/login.service'; import { LoginDto, GitHubOAuthDto, ForgotPasswordDto, ResetPasswordDto, ChangePasswordDto, VerificationCodeLoginDto, SendLoginVerificationCodeDto, RefreshTokenDto, EmailAddressDto, } from './dto/login.dto'; import { LoginResponseDto, GitHubOAuthResponseDto, ForgotPasswordResponseDto, CommonResponseDto, RefreshTokenResponseDto } from './dto/login_response.dto'; import { Throttle, ThrottlePresets } from '../../core/security_core/throttle.decorator'; import { Timeout, TimeoutPresets } from '../../core/security_core/timeout.decorator'; // 错误代码到HTTP状态码的映射 const ERROR_STATUS_MAP = { LOGIN_FAILED: HttpStatus.UNAUTHORIZED, TEST_MODE_ONLY: HttpStatus.PARTIAL_CONTENT, TOKEN_REFRESH_FAILED: HttpStatus.UNAUTHORIZED, GITHUB_OAUTH_FAILED: HttpStatus.UNAUTHORIZED, SEND_CODE_FAILED: HttpStatus.BAD_REQUEST, RESET_PASSWORD_FAILED: HttpStatus.BAD_REQUEST, CHANGE_PASSWORD_FAILED: HttpStatus.BAD_REQUEST, VERIFICATION_CODE_LOGIN_FAILED: HttpStatus.UNAUTHORIZED, INVALID_VERIFICATION_CODE: HttpStatus.BAD_REQUEST, } as const; @ApiTags('auth') @Controller('auth') export class LoginController { private readonly logger = new Logger(LoginController.name); constructor(private readonly loginService: LoginService) {} /** * 通用响应处理方法 * * 职责: * - 根据业务结果设置HTTP状态码 * - 处理不同类型的错误响应 * - 统一响应格式和错误处理 * * @param result 业务服务返回的结果 * @param res Express响应对象 * @param successStatus 成功时的HTTP状态码,默认为200 * @private */ private handleResponse(result: any, res: Response, successStatus: HttpStatus = HttpStatus.OK): void { if (result.success) { res.status(successStatus).json(result); return; } const statusCode = this.getErrorStatusCode(result); res.status(statusCode).json(result); } /** * 根据错误代码和消息获取HTTP状态码 * * @param result 业务服务返回的结果 * @returns HTTP状态码 * @private */ private getErrorStatusCode(result: any): HttpStatus { if (result.error_code && ERROR_STATUS_MAP[result.error_code as keyof typeof ERROR_STATUS_MAP]) { return ERROR_STATUS_MAP[result.error_code as keyof typeof ERROR_STATUS_MAP]; } if (result.message?.includes('已存在') || result.message?.includes('已被注册')) { return HttpStatus.CONFLICT; } if (result.message?.includes('令牌验证失败') || result.message?.includes('已过期')) { return HttpStatus.UNAUTHORIZED; } if (result.message?.includes('用户不存在')) { return HttpStatus.NOT_FOUND; } return HttpStatus.BAD_REQUEST; } /** * 用户登录 * * @param loginDto 登录数据 * @param res Express响应对象 */ @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_PER_ACCOUNT) @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 }); this.handleResponse(result, res); } /** * GitHub OAuth登录 * * @param githubDto GitHub OAuth数据 * @param res Express响应对象 */ @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 }); this.handleResponse(result, res); } /** * 发送密码重置验证码 * * @param forgotPasswordDto 忘记密码数据 * @param res Express响应对象 */ @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); this.handleResponse(result, res); } /** * 重置密码 * * @param resetPasswordDto 重置密码数据 * @param res Express响应对象 */ @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 }); this.handleResponse(result, res); } /** * 修改密码 * * @param changePasswordDto 修改密码数据 * @param res Express响应对象 */ @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 { const userId = BigInt(changePasswordDto.user_id); const result = await this.loginService.changePassword( userId, changePasswordDto.old_password, changePasswordDto.new_password ); this.handleResponse(result, res); } /** * 验证码登录 * * @param verificationCodeLoginDto 验证码登录数据 * @param res Express响应对象 */ @ApiOperation({ summary: '验证码登录', description: '使用邮箱或手机号和验证码进行登录,无需密码' }) @ApiBody({ type: VerificationCodeLoginDto }) @SwaggerApiResponse({ status: 200, description: '验证码登录成功', type: LoginResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 401, description: '验证码错误或已过期' }) @SwaggerApiResponse({ status: 404, description: '用户不存在' }) @Post('verification-code-login') @HttpCode(HttpStatus.OK) @UsePipes(new ValidationPipe({ transform: true })) async verificationCodeLogin( @Body() verificationCodeLoginDto: VerificationCodeLoginDto, @Res() res: Response ): Promise { const result = await this.loginService.verificationCodeLogin({ identifier: verificationCodeLoginDto.identifier, verificationCode: verificationCodeLoginDto.verification_code }); this.handleResponse(result, res); } /** * 发送登录验证码 * * @param sendLoginVerificationCodeDto 发送验证码数据 * @param res Express响应对象 */ @ApiOperation({ summary: '发送登录验证码', description: '向用户邮箱或手机发送登录验证码' }) @ApiBody({ type: SendLoginVerificationCodeDto }) @SwaggerApiResponse({ status: 200, description: '验证码发送成功', type: ForgotPasswordResponseDto }) @SwaggerApiResponse({ status: 206, description: '测试模式:验证码已生成但未真实发送', type: ForgotPasswordResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 404, description: '用户不存在' }) @SwaggerApiResponse({ status: 429, description: '发送频率过高' }) @Post('send-login-verification-code') @UsePipes(new ValidationPipe({ transform: true })) async sendLoginVerificationCode( @Body() sendLoginVerificationCodeDto: SendLoginVerificationCodeDto, @Res() res: Response ): Promise { const result = await this.loginService.sendLoginVerificationCode(sendLoginVerificationCodeDto.identifier); this.handleResponse(result, res); } /** * 刷新访问令牌 * * @param refreshTokenDto 刷新令牌数据 * @param res Express响应对象 */ @ApiOperation({ summary: '刷新访问令牌', description: '使用有效的刷新令牌生成新的访问令牌' }) @ApiBody({ type: RefreshTokenDto }) @SwaggerApiResponse({ status: 200, description: '令牌刷新成功', type: RefreshTokenResponseDto }) @SwaggerApiResponse({ status: 400, description: '请求参数错误' }) @SwaggerApiResponse({ status: 401, description: '刷新令牌无效或已过期' }) @SwaggerApiResponse({ status: 404, description: '用户不存在或已被禁用' }) @SwaggerApiResponse({ status: 429, description: '刷新请求过于频繁' }) @Throttle(ThrottlePresets.REFRESH_TOKEN) @Timeout(TimeoutPresets.NORMAL) @Post('refresh-token') @UsePipes(new ValidationPipe({ transform: true })) async refreshToken(@Body() refreshTokenDto: RefreshTokenDto, @Res() res: Response): Promise { const result = await this.loginService.refreshAccessToken(refreshTokenDto.refresh_token); this.handleResponse(result, res); } /** * 调试验证码信息(仅开发环境) * * @param sendEmailVerificationDto 邮箱信息 * @param res Express响应对象 */ @ApiOperation({ summary: '调试验证码信息', description: '获取验证码的详细调试信息(仅开发环境)' }) @ApiBody({ type: EmailAddressDto }) @Post('debug-verification-code') @UsePipes(new ValidationPipe({ transform: true })) async debugVerificationCode( @Body() sendEmailVerificationDto: EmailAddressDto, @Res() res: Response ): Promise { const result = await this.loginService.debugVerificationCode(sendEmailVerificationDto.email); res.status(HttpStatus.OK).json(result); } }