504 lines
14 KiB
TypeScript
504 lines
14 KiB
TypeScript
/**
|
||
* 登录网关控制器
|
||
*
|
||
* 架构层级: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<void> {
|
||
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<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
|
||
});
|
||
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
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<void> {
|
||
const result = await this.loginService.debugVerificationCode(sendEmailVerificationDto.email);
|
||
res.status(HttpStatus.OK).json(result);
|
||
}
|
||
}
|