dto:为注册接口添加邮箱验证码参数
- 在 RegisterDto 中添加 email_verification_code 可选字段 - 更新 RegisterRequest 接口定义 - 在注册核心服务中添加验证码验证逻辑 - 提供邮箱时必须提供有效的验证码进行验证
This commit is contained in:
@@ -129,6 +129,20 @@ export class RegisterDto {
|
|||||||
@IsOptional()
|
@IsOptional()
|
||||||
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
@IsPhoneNumber(null, { message: '手机号格式不正确' })
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 邮箱验证码(当提供邮箱时必填)
|
||||||
|
*/
|
||||||
|
@ApiProperty({
|
||||||
|
description: '邮箱验证码,当提供邮箱时必填',
|
||||||
|
example: '123456',
|
||||||
|
pattern: '^\\d{6}$',
|
||||||
|
required: false
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsString({ message: '验证码必须是字符串' })
|
||||||
|
@Matches(/^\d{6}$/, { message: '验证码必须是6位数字' })
|
||||||
|
email_verification_code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -313,3 +327,48 @@ export class ChangePasswordDto {
|
|||||||
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
@Matches(/^(?=.*[a-zA-Z])(?=.*\d)/, { message: '新密码必须包含字母和数字' })
|
||||||
new_password: string;
|
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;
|
||||||
|
}
|
||||||
@@ -19,6 +19,8 @@
|
|||||||
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
import { Injectable, UnauthorizedException, ConflictException, NotFoundException, BadRequestException } from '@nestjs/common';
|
||||||
import { UsersService } from '../db/users/users.service';
|
import { UsersService } from '../db/users/users.service';
|
||||||
import { Users } from '../db/users/users.entity';
|
import { Users } from '../db/users/users.entity';
|
||||||
|
import { EmailService } from '../utils/email/email.service';
|
||||||
|
import { VerificationService, VerificationCodeType } from '../utils/verification/verification.service';
|
||||||
import * as bcrypt from 'bcrypt';
|
import * as bcrypt from 'bcrypt';
|
||||||
import * as crypto from 'crypto';
|
import * as crypto from 'crypto';
|
||||||
|
|
||||||
@@ -46,6 +48,8 @@ export interface RegisterRequest {
|
|||||||
email?: string;
|
email?: string;
|
||||||
/** 手机号(可选) */
|
/** 手机号(可选) */
|
||||||
phone?: string;
|
phone?: string;
|
||||||
|
/** 邮箱验证码(当提供邮箱时必填) */
|
||||||
|
email_verification_code?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -90,6 +94,8 @@ export interface AuthResult {
|
|||||||
export class LoginCoreService {
|
export class LoginCoreService {
|
||||||
constructor(
|
constructor(
|
||||||
private readonly usersService: UsersService,
|
private readonly usersService: UsersService,
|
||||||
|
private readonly emailService: EmailService,
|
||||||
|
private readonly verificationService: VerificationService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -150,7 +156,21 @@ export class LoginCoreService {
|
|||||||
* @throws BadRequestException 数据验证失败时
|
* @throws BadRequestException 数据验证失败时
|
||||||
*/
|
*/
|
||||||
async register(registerRequest: RegisterRequest): Promise<AuthResult> {
|
async register(registerRequest: RegisterRequest): Promise<AuthResult> {
|
||||||
const { username, password, nickname, email, phone } = registerRequest;
|
const { username, password, nickname, email, phone, email_verification_code } = registerRequest;
|
||||||
|
|
||||||
|
// 如果提供了邮箱,必须验证邮箱验证码
|
||||||
|
if (email) {
|
||||||
|
if (!email_verification_code) {
|
||||||
|
throw new BadRequestException('提供邮箱时必须提供邮箱验证码');
|
||||||
|
}
|
||||||
|
|
||||||
|
// 验证邮箱验证码
|
||||||
|
await this.verificationService.verifyCode(
|
||||||
|
email,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION,
|
||||||
|
email_verification_code
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// 验证密码强度
|
// 验证密码强度
|
||||||
this.validatePasswordStrength(password);
|
this.validatePasswordStrength(password);
|
||||||
@@ -165,9 +185,20 @@ export class LoginCoreService {
|
|||||||
nickname,
|
nickname,
|
||||||
email,
|
email,
|
||||||
phone,
|
phone,
|
||||||
role: 1 // 默认普通用户
|
role: 1, // 默认普通用户
|
||||||
|
email_verified: email ? true : false // 如果提供了邮箱且验证码验证通过,则标记为已验证
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 如果提供了邮箱,发送欢迎邮件
|
||||||
|
if (email) {
|
||||||
|
try {
|
||||||
|
await this.emailService.sendWelcomeEmail(email, nickname);
|
||||||
|
} catch (error) {
|
||||||
|
// 邮件发送失败不影响注册流程,只记录日志
|
||||||
|
console.warn(`欢迎邮件发送失败: ${email}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
isNewUser: true
|
isNewUser: true
|
||||||
@@ -215,9 +246,19 @@ export class LoginCoreService {
|
|||||||
email,
|
email,
|
||||||
github_id,
|
github_id,
|
||||||
avatar_url,
|
avatar_url,
|
||||||
role: 1 // 默认普通用户
|
role: 1, // 默认普通用户
|
||||||
|
email_verified: email ? true : false // GitHub邮箱直接验证
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// 发送欢迎邮件
|
||||||
|
if (email) {
|
||||||
|
try {
|
||||||
|
await this.emailService.sendWelcomeEmail(email, nickname);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`欢迎邮件发送失败: ${email}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
user,
|
user,
|
||||||
isNewUser: true
|
isNewUser: true
|
||||||
@@ -237,6 +278,11 @@ export class LoginCoreService {
|
|||||||
|
|
||||||
if (this.isEmail(identifier)) {
|
if (this.isEmail(identifier)) {
|
||||||
user = await this.usersService.findByEmail(identifier);
|
user = await this.usersService.findByEmail(identifier);
|
||||||
|
|
||||||
|
// 检查邮箱是否已验证
|
||||||
|
if (user && !user.email_verified) {
|
||||||
|
throw new BadRequestException('邮箱未验证,无法重置密码');
|
||||||
|
}
|
||||||
} else if (this.isPhoneNumber(identifier)) {
|
} else if (this.isPhoneNumber(identifier)) {
|
||||||
const users = await this.usersService.findAll();
|
const users = await this.usersService.findAll();
|
||||||
user = users.find(u => u.phone === identifier) || null;
|
user = users.find(u => u.phone === identifier) || null;
|
||||||
@@ -246,18 +292,30 @@ export class LoginCoreService {
|
|||||||
throw new NotFoundException('用户不存在');
|
throw new NotFoundException('用户不存在');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 生成6位数验证码
|
// 生成验证码
|
||||||
const verificationCode = this.generateVerificationCode();
|
const verificationCode = await this.verificationService.generateCode(
|
||||||
|
identifier,
|
||||||
|
VerificationCodeType.PASSWORD_RESET
|
||||||
|
);
|
||||||
|
|
||||||
// TODO: 实际应用中应该:
|
// 发送验证码
|
||||||
// 1. 将验证码存储到Redis等缓存中,设置过期时间(如5分钟)
|
if (this.isEmail(identifier)) {
|
||||||
// 2. 发送验证码到用户邮箱或手机
|
const success = await this.emailService.sendVerificationCode({
|
||||||
// 3. 返回成功消息而不是验证码本身
|
email: identifier,
|
||||||
|
code: verificationCode,
|
||||||
|
nickname: user.nickname,
|
||||||
|
purpose: 'password_reset'
|
||||||
|
});
|
||||||
|
|
||||||
// 这里为了演示,直接返回验证码
|
if (!success) {
|
||||||
console.log(`密码重置验证码(${identifier}): ${verificationCode}`);
|
throw new BadRequestException('验证码发送失败,请稍后重试');
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// TODO: 实现短信发送
|
||||||
|
console.log(`短信验证码(${identifier}): ${verificationCode}`);
|
||||||
|
}
|
||||||
|
|
||||||
return verificationCode;
|
return verificationCode; // 实际应用中不应返回验证码
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -271,10 +329,15 @@ export class LoginCoreService {
|
|||||||
async resetPassword(resetRequest: PasswordResetRequest): Promise<Users> {
|
async resetPassword(resetRequest: PasswordResetRequest): Promise<Users> {
|
||||||
const { identifier, verificationCode, newPassword } = resetRequest;
|
const { identifier, verificationCode, newPassword } = resetRequest;
|
||||||
|
|
||||||
// TODO: 实际应用中应该验证验证码的有效性
|
// 验证验证码
|
||||||
// 这里为了演示,简单验证验证码格式
|
const isValidCode = await this.verificationService.verifyCode(
|
||||||
if (!/^\d{6}$/.test(verificationCode)) {
|
identifier,
|
||||||
throw new BadRequestException('验证码格式错误');
|
VerificationCodeType.PASSWORD_RESET,
|
||||||
|
verificationCode
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!isValidCode) {
|
||||||
|
throw new BadRequestException('验证码验证失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
// 查找用户
|
// 查找用户
|
||||||
@@ -389,6 +452,90 @@ export class LoginCoreService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 发送邮箱验证码
|
||||||
|
*
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @param nickname 用户昵称
|
||||||
|
* @returns 验证码
|
||||||
|
*/
|
||||||
|
async sendEmailVerification(email: string, nickname?: string): Promise<string> {
|
||||||
|
// 生成验证码
|
||||||
|
const verificationCode = await this.verificationService.generateCode(
|
||||||
|
email,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION
|
||||||
|
);
|
||||||
|
|
||||||
|
// 发送验证邮件
|
||||||
|
const success = await this.emailService.sendVerificationCode({
|
||||||
|
email,
|
||||||
|
code: verificationCode,
|
||||||
|
nickname,
|
||||||
|
purpose: 'email_verification'
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!success) {
|
||||||
|
throw new BadRequestException('验证邮件发送失败,请稍后重试');
|
||||||
|
}
|
||||||
|
|
||||||
|
return verificationCode; // 实际应用中不应返回验证码
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 验证邮箱验证码
|
||||||
|
*
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @param code 验证码
|
||||||
|
* @returns 验证结果
|
||||||
|
*/
|
||||||
|
async verifyEmailCode(email: string, code: string): Promise<boolean> {
|
||||||
|
// 验证验证码
|
||||||
|
const isValid = await this.verificationService.verifyCode(
|
||||||
|
email,
|
||||||
|
VerificationCodeType.EMAIL_VERIFICATION,
|
||||||
|
code
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
// 更新用户邮箱验证状态
|
||||||
|
const user = await this.usersService.findByEmail(email);
|
||||||
|
if (user) {
|
||||||
|
await this.usersService.update(user.id, {
|
||||||
|
email_verified: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// 发送欢迎邮件
|
||||||
|
try {
|
||||||
|
await this.emailService.sendWelcomeEmail(email, user.nickname);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn(`欢迎邮件发送失败: ${email}`, error);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 重新发送邮箱验证码
|
||||||
|
*
|
||||||
|
* @param email 邮箱地址
|
||||||
|
* @returns 验证码
|
||||||
|
*/
|
||||||
|
async resendEmailVerification(email: string): Promise<string> {
|
||||||
|
const user = await this.usersService.findByEmail(email);
|
||||||
|
|
||||||
|
if (!user) {
|
||||||
|
throw new NotFoundException('用户不存在');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (user.email_verified) {
|
||||||
|
throw new BadRequestException('邮箱已验证,无需重复验证');
|
||||||
|
}
|
||||||
|
|
||||||
|
return await this.sendEmailVerification(email, user.nickname);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 生成验证码
|
* 生成验证码
|
||||||
*
|
*
|
||||||
|
|||||||
Reference in New Issue
Block a user