Initial WhaleTown V2 backend
This commit is contained in:
55
src/gateway/auth/auth.gateway.module.ts
Normal file
55
src/gateway/auth/auth.gateway.module.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
/**
|
||||
* 认证网关模块
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 整合所有认证相关的网关组件
|
||||
* - 提供HTTP API接口
|
||||
* - 配置认证守卫和中间件
|
||||
* - 处理请求验证和响应格式化
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于HTTP协议处理和API网关功能
|
||||
* - 依赖业务层服务,不包含业务逻辑
|
||||
* - 提供统一的API入口和文档
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 Business Layer 的 AuthModule
|
||||
* - 提供 Controller 和 Guard
|
||||
*
|
||||
* @author moyin
|
||||
* @version 2.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import { Module } from '@nestjs/common';
|
||||
import { LoginController } from './login.controller';
|
||||
import { RegisterController } from './register.controller';
|
||||
import { JwtAuthGuard } from './jwt_auth.guard';
|
||||
import { AuthModule } from '../../business/auth/auth.module';
|
||||
import { LoginCoreModule } from '../../core/login_core/login_core.module';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
// 导入业务层模块
|
||||
AuthModule,
|
||||
// 导入核心层模块(JwtAuthGuard需要LoginCoreService)
|
||||
LoginCoreModule,
|
||||
],
|
||||
controllers: [
|
||||
// 网关层控制器
|
||||
LoginController,
|
||||
RegisterController,
|
||||
],
|
||||
providers: [
|
||||
// 认证守卫
|
||||
JwtAuthGuard,
|
||||
],
|
||||
exports: [
|
||||
// 导出守卫供其他模块使用
|
||||
JwtAuthGuard,
|
||||
],
|
||||
})
|
||||
export class AuthGatewayModule {}
|
||||
69
src/gateway/auth/current_user.decorator.ts
Normal file
69
src/gateway/auth/current_user.decorator.ts
Normal file
@@ -0,0 +1,69 @@
|
||||
/**
|
||||
* 当前用户装饰器
|
||||
*
|
||||
* 功能描述:
|
||||
* - 从请求上下文中提取当前认证用户信息
|
||||
* - 简化控制器中获取用户信息的操作
|
||||
* - 支持获取用户对象的特定属性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于用户信息提取和参数装饰
|
||||
* - 提供类型安全的用户信息访问
|
||||
* - 简化控制器方法的参数处理
|
||||
*
|
||||
* 使用示例:
|
||||
* ```typescript
|
||||
* @Get('profile')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProfile(@CurrentUser() user: JwtPayload) {
|
||||
* return { user };
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 文件重命名为snake_case格式,更新注释规范
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { createParamDecorator, ExecutionContext } from '@nestjs/common';
|
||||
import { JwtPayload } from '../../core/login_core/login_core.service';
|
||||
import { AuthenticatedRequest } from './jwt_auth.guard';
|
||||
|
||||
/**
|
||||
* 当前用户装饰器实现
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从执行上下文获取HTTP请求对象
|
||||
* 2. 提取请求中的用户信息(由JwtAuthGuard注入)
|
||||
* 3. 根据data参数返回完整用户对象或特定属性
|
||||
* 4. 提供类型安全的用户信息访问
|
||||
*
|
||||
* @param data 可选的属性名,用于获取用户对象的特定属性
|
||||
* @param ctx 执行上下文,包含HTTP请求信息
|
||||
* @returns JwtPayload | any 用户信息或用户的特定属性
|
||||
* @throws 无异常抛出,依赖JwtAuthGuard确保用户信息存在
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 获取完整用户对象
|
||||
* @Get('profile')
|
||||
* getProfile(@CurrentUser() user: JwtPayload) { }
|
||||
*
|
||||
* // 获取特定属性
|
||||
* @Get('username')
|
||||
* getUsername(@CurrentUser('username') username: string) { }
|
||||
* ```
|
||||
*/
|
||||
export const CurrentUser = createParamDecorator(
|
||||
(data: keyof JwtPayload | undefined, ctx: ExecutionContext) => {
|
||||
const request = ctx.switchToHttp().getRequest<AuthenticatedRequest>();
|
||||
const user = request.user;
|
||||
|
||||
return data ? user?.[data] : user;
|
||||
},
|
||||
);
|
||||
469
src/gateway/auth/dto/login.dto.ts
Normal file
469
src/gateway/auth/dto/login.dto.ts
Normal file
@@ -0,0 +1,469 @@
|
||||
/**
|
||||
* 登录业务数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的请求数据结构
|
||||
* - 提供数据验证规则和错误提示
|
||||
* - 确保API接口的数据格式一致性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于数据结构定义和验证规则
|
||||
* - 提供Swagger文档生成支持
|
||||
* - 确保类型安全和数据完整性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,修正作者信息
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
IsString,
|
||||
IsEmail,
|
||||
IsPhoneNumber,
|
||||
IsNotEmpty,
|
||||
Length,
|
||||
IsOptional,
|
||||
Matches,
|
||||
IsNumberString
|
||||
} from 'class-validator';
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 登录请求DTO
|
||||
*/
|
||||
export class LoginDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持用户名、邮箱或手机号登录
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '登录标识符,支持用户名、邮箱或手机号',
|
||||
example: 'testuser',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户密码',
|
||||
example: 'password123',
|
||||
minLength: 1,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@Length(1, 128, { message: '密码长度需在1-128字符之间' })
|
||||
password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册请求DTO
|
||||
*/
|
||||
export class RegisterDto {
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户名,只能包含字母、数字和下划线',
|
||||
example: 'testuser',
|
||||
minLength: 1,
|
||||
maxLength: 50,
|
||||
pattern: '^[a-zA-Z0-9_]+$'
|
||||
})
|
||||
@IsString({ message: '用户名必须是字符串' })
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
@Matches(/^[a-zA-Z0-9_]+$/, { message: '用户名只能包含字母、数字和下划线' })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '密码,必须包含字母和数字,长度8-128字符',
|
||||
example: 'password123',
|
||||
minLength: 8,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '密码不能为空' })
|
||||
@Length(8, 128, { message: '密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '密码必须包含字母和数字' })
|
||||
password: string;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户昵称',
|
||||
example: '测试用户',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '昵称必须是字符串' })
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 邮箱
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱地址,注册前必须先发送验证码',
|
||||
example: 'test@example.com',
|
||||
required: true
|
||||
})
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* 初始角色皮肤ID(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '初始角色皮肤ID(可选)',
|
||||
example: 'classic_whale',
|
||||
required: false,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '皮肤ID必须是字符串' })
|
||||
@Length(1, 100, { message: '皮肤ID长度需在1-100字符之间' })
|
||||
@Matches(/^[A-Za-z0-9_:-]+$/, { message: '皮肤ID格式不正确' })
|
||||
skin_id?: string;
|
||||
|
||||
/**
|
||||
* 手机号(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '手机号码(可选)',
|
||||
example: '+8613800138000',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||
phone?: string;
|
||||
|
||||
/**
|
||||
* 邮箱验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱验证码,必须先通过 /auth/send-email-verification 发送到注册邮箱',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$',
|
||||
required: true
|
||||
})
|
||||
@IsNotEmpty({ message: '邮箱验证码不能为空' })
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
email_verification_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth登录请求DTO
|
||||
*/
|
||||
export class GitHubOAuthDto {
|
||||
/**
|
||||
* GitHub用户ID
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub用户ID',
|
||||
example: '12345678',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: 'GitHub ID必须是字符串' })
|
||||
@IsNotEmpty({ message: 'GitHub ID不能为空' })
|
||||
@Length(1, 100, { message: 'GitHub ID长度需在1-100字符之间' })
|
||||
github_id: string;
|
||||
|
||||
/**
|
||||
* 用户名
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub用户名',
|
||||
example: 'octocat',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '用户名必须是字符串' })
|
||||
@IsNotEmpty({ message: '用户名不能为空' })
|
||||
@Length(1, 50, { message: '用户名长度需在1-50字符之间' })
|
||||
username: string;
|
||||
|
||||
/**
|
||||
* 昵称
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub显示名称',
|
||||
example: 'The Octocat',
|
||||
minLength: 1,
|
||||
maxLength: 50
|
||||
})
|
||||
@IsString({ message: '昵称必须是字符串' })
|
||||
@IsNotEmpty({ message: '昵称不能为空' })
|
||||
@Length(1, 50, { message: '昵称长度需在1-50字符之间' })
|
||||
nickname: string;
|
||||
|
||||
/**
|
||||
* 邮箱(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub邮箱地址(可选)',
|
||||
example: 'octocat@github.com',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
email?: string;
|
||||
|
||||
/**
|
||||
* 头像URL(可选)
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'GitHub头像URL(可选)',
|
||||
example: 'https://github.com/images/error/octocat_happy.gif',
|
||||
required: false
|
||||
})
|
||||
@IsOptional()
|
||||
@IsString({ message: '头像URL必须是字符串' })
|
||||
avatar_url?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码请求DTO
|
||||
*/
|
||||
export class ForgotPasswordDto {
|
||||
/**
|
||||
* 邮箱或手机号
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
|
||||
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码请求DTO
|
||||
*/
|
||||
export class ResetPasswordDto {
|
||||
/**
|
||||
* 邮箱或手机号
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '邮箱或手机号不能为空' })
|
||||
@Length(1, 100, { message: '标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '6位数字验证码',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$'
|
||||
})
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '新密码,必须包含字母和数字,长度8-128字符',
|
||||
example: 'newpassword123',
|
||||
minLength: 8,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '新密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 修改密码请求DTO
|
||||
*/
|
||||
export class ChangePasswordDto {
|
||||
/**
|
||||
* 用户ID
|
||||
* 实际应用中应从JWT令牌中获取,这里为了演示放在请求体中
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '用户ID(实际应用中应从JWT令牌中获取)',
|
||||
example: '1'
|
||||
})
|
||||
@IsNumberString({}, { message: '用户ID必须是数字字符串' })
|
||||
@IsNotEmpty({ message: '用户ID不能为空' })
|
||||
user_id: string;
|
||||
|
||||
/**
|
||||
* 旧密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '当前密码',
|
||||
example: 'oldpassword123',
|
||||
minLength: 1,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '旧密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '旧密码不能为空' })
|
||||
@Length(1, 128, { message: '旧密码长度需在1-128字符之间' })
|
||||
old_password: string;
|
||||
|
||||
/**
|
||||
* 新密码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '新密码,必须包含字母和数字,长度8-128字符',
|
||||
example: 'newpassword123',
|
||||
minLength: 8,
|
||||
maxLength: 128
|
||||
})
|
||||
@IsString({ message: '新密码必须是字符串' })
|
||||
@IsNotEmpty({ message: '新密码不能为空' })
|
||||
@Length(8, 128, { message: '新密码长度需在8-128字符之间' })
|
||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||
new_password: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 邮箱验证请求DTO
|
||||
*/
|
||||
export class EmailVerificationDto {
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'test@example.com'
|
||||
})
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
email: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '6位数字验证码',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$'
|
||||
})
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码请求DTO
|
||||
*/
|
||||
export class SendEmailVerificationDto {
|
||||
/**
|
||||
* 邮箱地址
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'test@example.com'
|
||||
})
|
||||
@IsEmail({}, { message: '邮箱格式不正确' })
|
||||
@IsNotEmpty({ message: '邮箱不能为空' })
|
||||
email: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证码登录请求DTO
|
||||
*/
|
||||
export class VerificationCodeLoginDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持邮箱或手机号登录
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '登录标识符,支持邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
|
||||
/**
|
||||
* 验证码
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '6位数字验证码',
|
||||
example: '123456',
|
||||
pattern: '^\\d{6}$'
|
||||
})
|
||||
@IsString({ message: '验证码必须是字符串' })
|
||||
@IsNotEmpty({ message: '验证码不能为空' })
|
||||
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||
verification_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送登录验证码请求DTO
|
||||
*/
|
||||
export class SendLoginVerificationCodeDto {
|
||||
/**
|
||||
* 登录标识符
|
||||
* 支持邮箱或手机号
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: '登录标识符,支持邮箱或手机号',
|
||||
example: 'test@example.com',
|
||||
minLength: 1,
|
||||
maxLength: 100
|
||||
})
|
||||
@IsString({ message: '登录标识符必须是字符串' })
|
||||
@IsNotEmpty({ message: '登录标识符不能为空' })
|
||||
@Length(1, 100, { message: '登录标识符长度需在1-100字符之间' })
|
||||
identifier: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 刷新令牌请求DTO
|
||||
*/
|
||||
export class RefreshTokenDto {
|
||||
/**
|
||||
* 刷新令牌
|
||||
*/
|
||||
@ApiProperty({
|
||||
description: 'JWT刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...',
|
||||
minLength: 1
|
||||
})
|
||||
@IsString({ message: '刷新令牌必须是字符串' })
|
||||
@IsNotEmpty({ message: '刷新令牌不能为空' })
|
||||
refresh_token: string;
|
||||
}
|
||||
479
src/gateway/auth/dto/login_response.dto.ts
Normal file
479
src/gateway/auth/dto/login_response.dto.ts
Normal file
@@ -0,0 +1,479 @@
|
||||
/**
|
||||
* 登录业务响应数据传输对象
|
||||
*
|
||||
* 功能描述:
|
||||
* - 定义登录相关API的响应数据结构
|
||||
* - 提供Swagger文档生成支持
|
||||
* - 确保API响应的数据格式一致性
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于响应数据结构定义
|
||||
* - 提供完整的API文档支持
|
||||
* - 确保响应格式的统一性
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 更新注释规范,修正作者信息
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-12-17
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
/**
|
||||
* 用户信息响应DTO
|
||||
*/
|
||||
export class UserInfoDto {
|
||||
@ApiProperty({
|
||||
description: '用户ID',
|
||||
example: '1'
|
||||
})
|
||||
id: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户名',
|
||||
example: 'testuser'
|
||||
})
|
||||
username: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户昵称',
|
||||
example: '测试用户'
|
||||
})
|
||||
nickname: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '邮箱地址',
|
||||
example: 'test@example.com',
|
||||
required: false
|
||||
})
|
||||
email?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '手机号码',
|
||||
example: '+8613800138000',
|
||||
required: false
|
||||
})
|
||||
phone?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '头像URL',
|
||||
example: 'https://example.com/avatar.jpg',
|
||||
required: false
|
||||
})
|
||||
avatar_url?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '用户角色',
|
||||
example: 1
|
||||
})
|
||||
role: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '创建时间',
|
||||
example: '2025-12-17T10:00:00.000Z'
|
||||
})
|
||||
created_at: Date;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应数据DTO
|
||||
*/
|
||||
export class LoginResponseDataDto {
|
||||
@ApiProperty({
|
||||
description: '用户信息',
|
||||
type: UserInfoDto
|
||||
})
|
||||
user: UserInfoDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT访问令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
refresh_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '访问令牌过期时间(秒)',
|
||||
example: 604800
|
||||
})
|
||||
expires_in: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '令牌类型',
|
||||
example: 'Bearer'
|
||||
})
|
||||
token_type: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '是否为新用户',
|
||||
example: false,
|
||||
required: false
|
||||
})
|
||||
is_new_user?: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '登录成功'
|
||||
})
|
||||
message: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 登录响应DTO
|
||||
*/
|
||||
export class LoginResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: LoginResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: LoginResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '登录成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'LOGIN_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 注册响应DTO
|
||||
*/
|
||||
export class RegisterResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: LoginResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: LoginResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '注册成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'REGISTER_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* GitHub OAuth响应DTO
|
||||
*/
|
||||
export class GitHubOAuthResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: LoginResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: LoginResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: 'GitHub登录成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'GITHUB_OAUTH_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码响应数据DTO
|
||||
*/
|
||||
export class ForgotPasswordResponseDataDto {
|
||||
@ApiProperty({
|
||||
description: '验证码(仅用于演示,实际应用中不应返回)',
|
||||
example: '123456',
|
||||
required: false
|
||||
})
|
||||
verification_code?: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '是否为测试模式',
|
||||
example: true,
|
||||
required: false
|
||||
})
|
||||
is_test_mode?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
* 忘记密码响应DTO
|
||||
*/
|
||||
export class ForgotPasswordResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: false,
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: true
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: false
|
||||
}
|
||||
}
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: ForgotPasswordResponseDataDto,
|
||||
required: false,
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: {
|
||||
verification_code: '123456',
|
||||
is_test_mode: false
|
||||
}
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: {
|
||||
verification_code: '059174',
|
||||
is_test_mode: true
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
data?: ForgotPasswordResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。',
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: '验证码已发送,请查收'
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。'
|
||||
}
|
||||
}
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'TEST_MODE_ONLY',
|
||||
examples: {
|
||||
success: {
|
||||
summary: '真实发送成功',
|
||||
value: null
|
||||
},
|
||||
testMode: {
|
||||
summary: '测试模式',
|
||||
value: 'TEST_MODE_ONLY'
|
||||
},
|
||||
failed: {
|
||||
summary: '发送失败',
|
||||
value: 'SEND_CODE_FAILED'
|
||||
}
|
||||
},
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 通用响应DTO(用于重置密码、修改密码等)
|
||||
*/
|
||||
export class CommonResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '操作成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'OPERATION_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 测试模式邮件验证码响应DTO
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2025-12-17: 功能新增 - 添加测试模式响应DTO (修改者: angjustinl)
|
||||
*/
|
||||
export class TestModeEmailVerificationResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功(测试模式下为false)',
|
||||
example: false
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
example: {
|
||||
verification_code: '059174',
|
||||
is_test_mode: true
|
||||
}
|
||||
})
|
||||
data: {
|
||||
verification_code: string;
|
||||
is_test_mode: boolean;
|
||||
};
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '⚠️ 测试模式:验证码已生成但未真实发送。请在控制台查看验证码,或配置邮件服务以启用真实发送。'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'TEST_MODE_ONLY'
|
||||
})
|
||||
error_code: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 成功发送邮件验证码响应DTO
|
||||
*/
|
||||
export class SuccessEmailVerificationResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
example: {
|
||||
verification_code: '123456',
|
||||
is_test_mode: false
|
||||
}
|
||||
})
|
||||
data: {
|
||||
verification_code: string;
|
||||
is_test_mode: boolean;
|
||||
};
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '验证码已发送,请查收'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: null,
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌刷新响应数据DTO
|
||||
*/
|
||||
export class RefreshTokenResponseDataDto {
|
||||
@ApiProperty({
|
||||
description: 'JWT访问令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
access_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: 'JWT刷新令牌',
|
||||
example: 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...'
|
||||
})
|
||||
refresh_token: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '访问令牌过期时间(秒)',
|
||||
example: 604800
|
||||
})
|
||||
expires_in: number;
|
||||
|
||||
@ApiProperty({
|
||||
description: '令牌类型',
|
||||
example: 'Bearer'
|
||||
})
|
||||
token_type: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 令牌刷新响应DTO
|
||||
*/
|
||||
export class RefreshTokenResponseDto {
|
||||
@ApiProperty({
|
||||
description: '请求是否成功',
|
||||
example: true
|
||||
})
|
||||
success: boolean;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应数据',
|
||||
type: RefreshTokenResponseDataDto,
|
||||
required: false
|
||||
})
|
||||
data?: RefreshTokenResponseDataDto;
|
||||
|
||||
@ApiProperty({
|
||||
description: '响应消息',
|
||||
example: '令牌刷新成功'
|
||||
})
|
||||
message: string;
|
||||
|
||||
@ApiProperty({
|
||||
description: '错误代码',
|
||||
example: 'TOKEN_REFRESH_FAILED',
|
||||
required: false
|
||||
})
|
||||
error_code?: string;
|
||||
}
|
||||
119
src/gateway/auth/jwt_auth.guard.ts
Normal file
119
src/gateway/auth/jwt_auth.guard.ts
Normal file
@@ -0,0 +1,119 @@
|
||||
/**
|
||||
* JWT 认证守卫
|
||||
*
|
||||
* 功能描述:
|
||||
* - 验证请求中的 JWT 令牌
|
||||
* - 提取用户信息并添加到请求上下文
|
||||
* - 保护需要认证的路由
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于JWT令牌验证和用户认证
|
||||
* - 提供统一的认证守卫机制
|
||||
* - 处理认证失败的异常情况
|
||||
*
|
||||
* 最近修改:
|
||||
* - 2026-01-07: 代码规范优化 - 文件夹扁平化,移除单文件文件夹结构
|
||||
* - 2026-01-07: 代码规范优化 - 文件重命名为snake_case格式,更新注释规范
|
||||
*
|
||||
* @author moyin
|
||||
* @version 1.0.2
|
||||
* @since 2025-01-05
|
||||
* @lastModified 2026-01-07
|
||||
*/
|
||||
|
||||
import {
|
||||
Injectable,
|
||||
CanActivate,
|
||||
ExecutionContext,
|
||||
UnauthorizedException,
|
||||
Logger,
|
||||
} from '@nestjs/common';
|
||||
import { Request } from 'express';
|
||||
import { LoginCoreService, JwtPayload } from '../../core/login_core/login_core.service';
|
||||
|
||||
/**
|
||||
* 扩展的请求接口,包含用户信息
|
||||
*/
|
||||
export interface AuthenticatedRequest extends Request {
|
||||
user: JwtPayload;
|
||||
}
|
||||
|
||||
@Injectable()
|
||||
export class JwtAuthGuard implements CanActivate {
|
||||
private readonly logger = new Logger(JwtAuthGuard.name);
|
||||
|
||||
constructor(private readonly loginCoreService: LoginCoreService) {}
|
||||
|
||||
/**
|
||||
* JWT令牌验证和用户认证
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 从请求头中提取Bearer令牌
|
||||
* 2. 验证令牌的有效性和签名
|
||||
* 3. 解码令牌获取用户信息
|
||||
* 4. 将用户信息添加到请求上下文
|
||||
* 5. 记录认证成功或失败的日志
|
||||
* 6. 返回认证结果
|
||||
*
|
||||
* @param context 执行上下文,包含HTTP请求信息
|
||||
* @returns Promise<boolean> 认证是否成功
|
||||
* @throws UnauthorizedException 当令牌缺失或无效时
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* @Get('protected')
|
||||
* @UseGuards(JwtAuthGuard)
|
||||
* getProtectedData() {
|
||||
* // 此方法需要有效的JWT令牌才能访问
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
async canActivate(context: ExecutionContext): Promise<boolean> {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const token = this.extractTokenFromHeader(request);
|
||||
|
||||
if (!token) {
|
||||
this.logger.warn('访问被拒绝:缺少认证令牌');
|
||||
throw new UnauthorizedException('缺少认证令牌');
|
||||
}
|
||||
|
||||
try {
|
||||
// 使用Core层服务验证JWT令牌
|
||||
const payload = await this.loginCoreService.verifyToken(token, 'access');
|
||||
|
||||
// 将用户信息添加到请求对象
|
||||
(request as AuthenticatedRequest).user = payload;
|
||||
|
||||
this.logger.log(`用户认证成功: ${payload.username} (ID: ${payload.sub})`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
const errorMessage = error instanceof Error ? error.message : '未知错误';
|
||||
this.logger.warn(`JWT 令牌验证失败: ${errorMessage}`);
|
||||
throw new UnauthorizedException('无效的认证令牌');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从请求头中提取JWT令牌
|
||||
*
|
||||
* 业务逻辑:
|
||||
* 1. 获取Authorization请求头
|
||||
* 2. 解析Bearer令牌格式
|
||||
* 3. 验证令牌类型是否为Bearer
|
||||
* 4. 返回提取的令牌字符串
|
||||
*
|
||||
* @param request HTTP请求对象
|
||||
* @returns string | undefined JWT令牌字符串或undefined
|
||||
* @throws 无异常抛出,返回undefined表示令牌不存在
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // 请求头格式:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...
|
||||
* const token = this.extractTokenFromHeader(request);
|
||||
* ```
|
||||
*/
|
||||
private extractTokenFromHeader(request: Request): string | undefined {
|
||||
const [type, token] = request.headers.authorization?.split(' ') ?? [];
|
||||
return type === 'Bearer' ? token : undefined;
|
||||
}
|
||||
}
|
||||
503
src/gateway/auth/login.controller.ts
Normal file
503
src/gateway/auth/login.controller.ts
Normal file
@@ -0,0 +1,503 @@
|
||||
/**
|
||||
* 登录网关控制器
|
||||
*
|
||||
* 架构层级: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,
|
||||
SendEmailVerificationDto
|
||||
} 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: 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);
|
||||
res.status(HttpStatus.OK).json(result);
|
||||
}
|
||||
}
|
||||
286
src/gateway/auth/register.controller.ts
Normal file
286
src/gateway/auth/register.controller.ts
Normal file
@@ -0,0 +1,286 @@
|
||||
/**
|
||||
* 注册网关控制器
|
||||
*
|
||||
* 架构层级:Gateway Layer(网关层)
|
||||
*
|
||||
* 功能描述:
|
||||
* - 处理用户注册相关的HTTP请求和响应
|
||||
* - 提供RESTful API接口
|
||||
* - 数据验证和格式化
|
||||
* - 邮箱验证功能
|
||||
*
|
||||
* 职责分离:
|
||||
* - 专注于HTTP协议处理和请求响应
|
||||
* - 调用业务层服务完成具体功能
|
||||
* - 处理API文档和参数验证
|
||||
* - 不包含业务逻辑,只做数据转换和路由
|
||||
*
|
||||
* 依赖关系:
|
||||
* - 依赖 Business Layer 的 RegisterService
|
||||
* - 使用 DTO 进行数据验证
|
||||
*
|
||||
* API端点:
|
||||
* - POST /auth/register - 用户注册
|
||||
* - POST /auth/send-email-verification - 发送邮箱验证码
|
||||
* - POST /auth/verify-email - 验证邮箱验证码
|
||||
* - POST /auth/resend-email-verification - 重新发送邮箱验证码
|
||||
*
|
||||
* @author moyin
|
||||
* @version 2.0.0
|
||||
* @since 2026-01-14
|
||||
* @lastModified 2026-01-14
|
||||
*/
|
||||
|
||||
import {
|
||||
Controller,
|
||||
Post,
|
||||
Body,
|
||||
HttpStatus,
|
||||
ValidationPipe,
|
||||
UsePipes,
|
||||
Logger,
|
||||
Res
|
||||
} from '@nestjs/common';
|
||||
import {
|
||||
ApiTags,
|
||||
ApiOperation,
|
||||
ApiResponse as SwaggerApiResponse,
|
||||
ApiBody
|
||||
} from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { RegisterService } from '../../business/auth/register.service';
|
||||
import {
|
||||
RegisterDto,
|
||||
EmailVerificationDto,
|
||||
SendEmailVerificationDto
|
||||
} from './dto/login.dto';
|
||||
import {
|
||||
RegisterResponseDto,
|
||||
CommonResponseDto,
|
||||
TestModeEmailVerificationResponseDto,
|
||||
SuccessEmailVerificationResponseDto
|
||||
} 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 = {
|
||||
REGISTER_FAILED: HttpStatus.BAD_REQUEST,
|
||||
TEST_MODE_ONLY: HttpStatus.PARTIAL_CONTENT,
|
||||
SEND_EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
|
||||
EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
|
||||
RESEND_EMAIL_VERIFICATION_FAILED: HttpStatus.BAD_REQUEST,
|
||||
INVALID_VERIFICATION_CODE: HttpStatus.BAD_REQUEST,
|
||||
} as const;
|
||||
|
||||
@ApiTags('auth')
|
||||
@Controller('auth')
|
||||
export class RegisterController {
|
||||
private readonly logger = new Logger(RegisterController.name);
|
||||
|
||||
constructor(private readonly registerService: RegisterService) {}
|
||||
|
||||
/**
|
||||
* 通用响应处理方法
|
||||
*
|
||||
* 职责:
|
||||
* - 根据业务结果设置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('用户不存在')) {
|
||||
return HttpStatus.NOT_FOUND;
|
||||
}
|
||||
|
||||
return HttpStatus.BAD_REQUEST;
|
||||
}
|
||||
|
||||
/**
|
||||
* 用户注册
|
||||
*
|
||||
* @param registerDto 注册数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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.registerService.register({
|
||||
username: registerDto.username,
|
||||
password: registerDto.password,
|
||||
nickname: registerDto.nickname,
|
||||
email: registerDto.email,
|
||||
phone: registerDto.phone,
|
||||
skin_id: registerDto.skin_id,
|
||||
email_verification_code: registerDto.email_verification_code
|
||||
});
|
||||
|
||||
this.handleResponse(result, res, HttpStatus.CREATED);
|
||||
}
|
||||
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
*
|
||||
* @param sendEmailVerificationDto 发送验证码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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_PER_EMAIL)
|
||||
@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.registerService.sendEmailVerification(sendEmailVerificationDto.email);
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证邮箱验证码
|
||||
*
|
||||
* @param emailVerificationDto 邮箱验证数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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.registerService.verifyEmailCode(
|
||||
emailVerificationDto.email,
|
||||
emailVerificationDto.verification_code
|
||||
);
|
||||
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
|
||||
/**
|
||||
* 重新发送邮箱验证码
|
||||
*
|
||||
* @param sendEmailVerificationDto 发送验证码数据
|
||||
* @param res Express响应对象
|
||||
*/
|
||||
@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)
|
||||
@Post('resend-email-verification')
|
||||
@UsePipes(new ValidationPipe({ transform: true }))
|
||||
async resendEmailVerification(
|
||||
@Body() sendEmailVerificationDto: SendEmailVerificationDto,
|
||||
@Res() res: Response
|
||||
): Promise<void> {
|
||||
const result = await this.registerService.resendEmailVerification(sendEmailVerificationDto.email);
|
||||
this.handleResponse(result, res);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user