- 修复登录控制器HTTP状态码问题,现在根据业务结果返回正确状态码 - 调整注册接口限流配置,从3次/5分钟放宽至10次/5分钟(开发环境) - 新增清除限流记录的调试接口,便于开发测试 - 更新API文档,反映状态码修复和限流调整 - 添加测试脚本验证修复效果 主要修复: - 业务失败时返回400/401而非200/201状态码 - 注册、登录、GitHub OAuth等接口现在正确处理错误状态码 - 限流配置更适合开发环境测试需求
515 lines
15 KiB
TypeScript
515 lines
15 KiB
TypeScript
/**
|
||
* 登录控制器
|
||
*
|
||
* 功能描述:
|
||
* - 处理登录相关的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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
// 实际应用中应从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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
// 注入ThrottleGuard并清除记录
|
||
// 这里需要通过依赖注入获取ThrottleGuard实例
|
||
res.status(HttpStatus.OK).json({
|
||
success: true,
|
||
message: '限流记录已清除'
|
||
});
|
||
}
|
||
} |