Skip to content

Commit 34cc494

Browse files
committed
feat: login email code need
1 parent de2ff9b commit 34cc494

File tree

20 files changed

+265
-95
lines changed

20 files changed

+265
-95
lines changed

apps/backend/src/app.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { AllExceptionFilter } from './common/filters/all-exception.filter'
77
import { TransformInterceptor } from './common/interceptors/transform.interceptor'
88
import { WinstonLogger } from './common/logger/winston-logger.service'
99
import { CookieModule } from './cookie/cookie.module'
10+
import { EmailCodeModule } from './email-code/email-code.module'
1011
import { EnvConfigModule } from './infra/env-config/env-config.module'
1112
import { PrismaModule } from './infra/prisma/prisma.module'
1213
import { RedisModule } from './infra/redis/redis.module'
@@ -38,6 +39,7 @@ import { UtilModule } from './util/util.module'
3839
ApiModule,
3940
UtilModule,
4041
ProxyModule,
42+
EmailCodeModule,
4143
],
4244
controllers: [AppController],
4345
providers: [

apps/backend/src/auth/auth.module.ts

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { Module } from '@nestjs/common'
22
import { AuthGuard } from '@/common/guards/auth.guard'
33
import { CookieModule } from '@/cookie/cookie.module'
4+
import { EmailCodeModule } from '@/email-code/email-code.module'
45
import { PrismaModule } from '@/infra/prisma/prisma.module'
56
import { SystemConfigModule } from '@/infra/system-config/system-config.module'
67
import { SessionModule } from '@/session/session.module'
@@ -13,6 +14,7 @@ import { AuthService } from './auth.service'
1314
PrismaModule,
1415
CookieModule,
1516
SystemConfigModule,
17+
EmailCodeModule,
1618
],
1719
controllers: [AuthController],
1820
providers: [AuthService, AuthGuard],

apps/backend/src/auth/auth.service.ts

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { compare, hash } from 'bcrypt'
44
import { HanaException } from '@/common/exceptions/hana.exception'
55
import { DEFAULT_COOKIE_MAX_AGE, REMEMBER_ME_COOKIE_MAX_AGE } from '@/constants/cookie'
66
import { RoleName } from '@/constants/role'
7+
import { EmailCodeService } from '@/email-code/email-code.service'
78
import { PrismaService } from '@/infra/prisma/prisma.service'
89
import { SystemConfigService } from '@/infra/system-config/system-config.service'
910
import { SessionService } from '@/session/session.service'
@@ -18,6 +19,7 @@ export class AuthService {
1819
private readonly prisma: PrismaService,
1920
private readonly sessionService: SessionService,
2021
private readonly systemConfigService: SystemConfigService,
22+
private readonly emailCodeService: EmailCodeService,
2123
) {}
2224

2325
/** 对密码进行哈希处理 */
@@ -209,14 +211,25 @@ export class AuthService {
209211

210212
/** 用户注册 */
211213
async register(dto: RegisterReqDto) {
212-
const registerEnabled = this.systemConfigService.get<boolean>(SystemConfigKey.REGISTER_ENABLED)
213-
if (!registerEnabled) {
214-
throw new HanaException('REGISTER_DISABLED')
215-
}
216-
217-
const { email, username, name, password, confirmPassword } = dto
214+
const { email, verificationCode, username, name, password, confirmPassword } = dto
218215

219216
try {
217+
// 系统配置校验
218+
// 1. 是否允许注册
219+
const registerEnabled = this.systemConfigService.get<boolean>(SystemConfigKey.REGISTER_ENABLED)
220+
if (!registerEnabled) {
221+
throw new HanaException('REGISTER_DISABLED')
222+
}
223+
224+
// 2. 是否需要邮箱验证
225+
const registerEmailVerify = this.systemConfigService.get<boolean>(SystemConfigKey.REGISTER_EMAIL_VERIFY)
226+
if (registerEmailVerify) {
227+
if (!verificationCode) {
228+
throw new HanaException('REGISTER_EMAIL_VERIFY_REQUIRED')
229+
}
230+
await this.emailCodeService.verifyEmailCode(email, verificationCode)
231+
}
232+
220233
// 验证密码确认
221234
if (password !== confirmPassword) {
222235
throw new HanaException('PASSWORD_MISMATCH')

apps/backend/src/auth/dto/register.dto.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,15 @@
1-
import { IsEmail, IsNotEmpty, IsString, Length, Matches, MinLength } from 'class-validator'
1+
import { IsEmail, IsNotEmpty, IsOptional, IsString, Length, Matches, MinLength } from 'class-validator'
22

33
export class RegisterReqDto {
44
@IsEmail({}, { message: '请输入有效的邮箱地址' })
55
@IsNotEmpty({ message: '邮箱不能为空' })
66
email: string
77

8+
@IsOptional()
9+
@IsString({ message: '验证码必须是字符串' })
10+
@Length(6, 6, { message: '验证码长度必须是 6 位数字' })
11+
verificationCode?: string
12+
813
@IsString({ message: '用户名必须是字符串' })
914
@Length(3, 20, { message: '用户名长度必须在3-20位之间' })
1015
@Matches(/^[\w-]+$/, {
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
import { IsEmail, IsNotEmpty } from 'class-validator'
2+
3+
export class EmailDto {
4+
@IsEmail({}, { message: '请输入有效的邮箱地址' })
5+
@IsNotEmpty({ message: '邮箱不能为空' })
6+
email: string
7+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
export * from './email.dto'
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { Body, Controller, Post } from '@nestjs/common'
2+
import { Public } from '@/common/decorators/public.decorator'
3+
import { ResMsg } from '@/common/decorators/res-msg.decorator'
4+
import { EmailDto } from './dto'
5+
import { EmailCodeService } from './email-code.service'
6+
7+
@Controller('email-code')
8+
export class EmailCodeController {
9+
constructor(private readonly emailCodeService: EmailCodeService) {}
10+
11+
/** 发送邮箱验证码 */
12+
@Public()
13+
@Post('send')
14+
@ResMsg('验证码已发送')
15+
async sendEmailCode(@Body() dto: EmailDto): Promise<void> {
16+
const { email } = dto
17+
await this.emailCodeService.sendEmailCode(email)
18+
}
19+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
import { Module } from '@nestjs/common'
2+
import { UtilModule } from '@/util/util.module'
3+
import { EmailCodeController } from './email-code.controller'
4+
import { EmailCodeService } from './email-code.service'
5+
6+
@Module({
7+
imports: [UtilModule],
8+
controllers: [EmailCodeController],
9+
providers: [EmailCodeService],
10+
exports: [EmailCodeService],
11+
})
12+
export class EmailCodeModule {}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
import { randomInt } from 'node:crypto'
2+
import { Inject, Injectable, Logger } from '@nestjs/common'
3+
import { Redis } from 'ioredis'
4+
import { HanaException } from '@/common/exceptions/hana.exception'
5+
import { REDIS_CLIENT } from '@/infra/redis/redis.module'
6+
import { UtilService } from '@/util/util.service'
7+
8+
@Injectable()
9+
export class EmailCodeService {
10+
private readonly logger = new Logger(EmailCodeService.name)
11+
private readonly verificationCodeTTL = 5 * 60 // 5 分钟,单位:秒
12+
13+
constructor(
14+
private readonly utilService: UtilService,
15+
@Inject(REDIS_CLIENT) private readonly redisClient: Redis,
16+
) {}
17+
18+
// [0, 1000000) 之间的安全整数,左侧补零至 6 位
19+
private genCode() {
20+
return randomInt(0, 1000000).toString().padStart(6, '0')
21+
}
22+
23+
// 获取 Redis 中验证码的 Key
24+
private getCodeKey(email: string) {
25+
return `email:code:${email}`
26+
}
27+
28+
/** 发送邮箱验证码 */
29+
async sendEmailCode(email: string) {
30+
try {
31+
const code = this.genCode()
32+
const key = this.getCodeKey(email)
33+
34+
await this.redisClient.setex(key, this.verificationCodeTTL, code)
35+
36+
await this.utilService.sendMail({
37+
to: email,
38+
subject: '【Apiplayer】账号安全验证码',
39+
text: `你的验证码是 ${code} ,5 分钟内有效。如非本人操作,请忽略本邮件。`,
40+
html: `<p>你的验证码是 <strong>${code}</strong> ,5 分钟内有效。</p><p>如果不是你本人操作,请尽快检查账号安全。</p>`,
41+
})
42+
43+
this.logger.log(`为邮箱 ${email} 发送了邮箱验证码`)
44+
}
45+
catch (error) {
46+
if (error instanceof HanaException) {
47+
throw error
48+
}
49+
this.logger.error('发送邮箱验证码失败:', error)
50+
throw new HanaException('INTERNAL_SERVER_ERROR')
51+
}
52+
}
53+
54+
/** 校验邮箱验证码 */
55+
async verifyEmailCode(email: string, code: string) {
56+
const key = this.getCodeKey(email)
57+
const storedCode = await this.redisClient.get(key)
58+
59+
if (!storedCode || storedCode !== code) {
60+
throw new HanaException('INVALID_VERIFICATION_CODE')
61+
}
62+
63+
// 校验通过后立即删除验证码
64+
await this.redisClient.del(key)
65+
}
66+
}

apps/backend/src/user/user.controller.ts

Lines changed: 1 addition & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Body, Controller, Get, Patch, Post, Query, UseGuards } from '@nestjs/common'
1+
import { Body, Controller, Get, Patch, Query, UseGuards } from '@nestjs/common'
22
import { plainToInstance } from 'class-transformer'
33
import { ReqUser } from '@/common/decorators/req-user.decorator'
44
import { UserDetailInfoDto } from '@/common/dto/user.dto'
@@ -29,14 +29,6 @@ export class UserController {
2929
return plainToInstance(UserDetailInfoDto, result)
3030
}
3131

32-
/** 发送用于敏感信息修改的邮箱验证码 */
33-
@Post('profile/verification-code')
34-
async sendProfileVerificationCode(
35-
@ReqUser('id') userId: string,
36-
): Promise<void> {
37-
await this.userService.sendProfileVerificationCode(userId)
38-
}
39-
4032
/** 分页搜索用户 */
4133
@Get('search')
4234
async searchUsers(

0 commit comments

Comments
 (0)