From f1457864dfef07ea85881f0d324a6382906d0578 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?=E7=A7=A6=E7=A7=8B=E6=97=AD?= <qiuxu.qin@outlook.com>
Date: Wed, 22 Feb 2023 16:09:31 +0800
Subject: [PATCH] update password

---
 src/email/dto/email.dto.ts           |  3 +-
 src/email/email.controller.ts        | 18 +++------
 src/email/email.service.ts           | 57 ++++++++++++++++------------
 src/users/dto/create-user.dto.ts     |  2 +-
 src/users/dto/update-password.dto.ts | 17 +++++++++
 src/users/users.controller.ts        | 23 ++++++++++-
 src/users/users.service.ts           | 55 +++++++++++++++++----------
 7 files changed, 115 insertions(+), 60 deletions(-)
 create mode 100644 src/users/dto/update-password.dto.ts

diff --git a/src/email/dto/email.dto.ts b/src/email/dto/email.dto.ts
index 9de4128..acde50c 100644
--- a/src/email/dto/email.dto.ts
+++ b/src/email/dto/email.dto.ts
@@ -3,7 +3,8 @@ import { IsEmail, IsNotEmpty } from 'class-validator'
 
 export enum EmailScene {
   register = 'register',
-  forgetPassword = 'forgetPassword',
+  updatePassword = 'updatePassword',
+  updateEmail = 'updateEmail',
 }
 
 export class EmailSendDto {
diff --git a/src/email/email.controller.ts b/src/email/email.controller.ts
index dc5db09..eab53b5 100644
--- a/src/email/email.controller.ts
+++ b/src/email/email.controller.ts
@@ -1,22 +1,16 @@
-import { Body, Controller, Query, Post } from '@nestjs/common'
+import { Body, Controller, Post } from '@nestjs/common'
 import { EmailService } from './email.service'
-import { ApiTags, ApiOperation, ApiQuery } from '@nestjs/swagger'
-import { EmailSendDto, EmailScene } from './dto/email.dto'
+import { ApiTags, ApiOperation } from '@nestjs/swagger'
+import { EmailSendDto } from './dto/email.dto'
 
 @ApiTags('Email')
 @Controller('api/email')
 export class EmailController {
   constructor(private readonly emailService: EmailService) {}
 
-  // @ApiOperation({ summary: '测试邮件' })
-  // @Post('test')
-  // async sendEmailTo(@Body() payload: EmailDto) {
-  //   return this.emailService.sendEmailTo(payload.email)
-  // }
-
   @ApiOperation({ summary: '发送邮箱验证码' })
-  @Post('verificationCode')
-  async getRegisterToken(@Body() payload: EmailSendDto) {
-    return this.emailService.getRegisterToken(payload.email, payload.scene)
+  @Post('verifyCode')
+  async sendEmailCode(@Body() payload: EmailSendDto) {
+    return this.emailService.sendEmailToken(payload.email, payload.scene)
   }
 }
diff --git a/src/email/email.service.ts b/src/email/email.service.ts
index dd15eb4..2b25a86 100644
--- a/src/email/email.service.ts
+++ b/src/email/email.service.ts
@@ -7,6 +7,11 @@ import { PrismaService } from 'nestjs-prisma'
 
 @Injectable()
 export class EmailService {
+  private subjectMap = {
+    [EmailScene.register]: '注册账号',
+    [EmailScene.updatePassword]: '修改密码',
+    [EmailScene.updateEmail]: '修改邮箱',
+  }
   constructor(
     private prismaService: PrismaService,
     private mailerService: MailerService,
@@ -15,42 +20,46 @@ export class EmailService {
     private secureConfig: SecurityConfig,
   ) {}
 
-  async sendEmailTo(email: string) {
-    return this.mailerService.sendMail({
-      to: email, // list of receivers
-      subject: 'Testing Nest Mailermodule with template ✔',
-      template: 'index', // The `.pug` or `.hbs` extension is appended automatically.
-      context: {
-        // Data to be sent to template engine.
-        code: 'cf1a3f828287',
-        username: 'john doe',
-      },
-    })
-  }
-
-  async getRegisterToken(email: string, scene: EmailScene) {
-    const user = await this.prismaService.user.findUnique({ where: { email } })
-    if (user) {
-      throw new ConflictException(`邮箱${email}已注册`)
+  async sendEmailToken(email: string, scene: EmailScene) {
+    switch (scene) {
+      case EmailScene.register:
+        const user = await this.prismaService.user.findUnique({
+          where: { email },
+        })
+        if (user) {
+          throw new ConflictException(`邮箱${email}已注册`)
+        }
+        break
+      case EmailScene.updatePassword:
+        await this.prismaService.user.findUniqueOrThrow({
+          where: { email },
+        })
+        break
+      case EmailScene.updateEmail:
+        await this.prismaService.user.findUniqueOrThrow({
+          where: { email },
+        })
+        break
     }
-    const verificationCode = this.generateVerificationCode()
+
+    const verifyCode = this.generateVerifyCode()
     const registerToken = this.jwtService.sign(
       { email, scene },
-      { secret: this.getEmailJwtSecret(verificationCode, scene) },
+      { secret: this.getEmailJwtSecret(verifyCode, scene) },
     )
     await this.mailerService.sendMail({
       to: email,
-      subject: '注册qiuxu.site',
-      html: `您正在注册qiuxu.site,验证码为 <strong>${verificationCode}</strong>,30分钟内有效`,
+      subject: `【qiuxu.site】${this.subjectMap[scene]}`,
+      html: `您正在qiuxu.site${this.subjectMap[scene]},验证码为 <strong>${verifyCode}</strong>,30分钟内有效`,
     })
     return registerToken
   }
 
-  getEmailJwtSecret(verificationCode: string, scene: EmailScene) {
-    return this.secureConfig.jwt_access_secret + verificationCode + scene
+  getEmailJwtSecret(verifyCode: string, scene: EmailScene) {
+    return this.secureConfig.jwt_access_secret + verifyCode + scene
   }
 
-  private generateVerificationCode() {
+  private generateVerifyCode() {
     return Math.floor(Math.random() * 899999 + 100000).toString()
   }
 }
diff --git a/src/users/dto/create-user.dto.ts b/src/users/dto/create-user.dto.ts
index d9f1a7a..95147dc 100644
--- a/src/users/dto/create-user.dto.ts
+++ b/src/users/dto/create-user.dto.ts
@@ -22,7 +22,7 @@ export class CreateUserDto {
   /** @description 验证码 */
   @IsNotEmpty()
   @Length(6, 6)
-  verificationCode: string
+  verifyCode: string
 
   /** @description 发送邮箱接口返回的Token */
   @IsNotEmpty()
diff --git a/src/users/dto/update-password.dto.ts b/src/users/dto/update-password.dto.ts
new file mode 100644
index 0000000..85ae7d8
--- /dev/null
+++ b/src/users/dto/update-password.dto.ts
@@ -0,0 +1,17 @@
+import { IsEmail, IsNotEmpty, IsStrongPassword } from 'class-validator'
+
+export class UpdatePassword {
+  @IsNotEmpty()
+  @IsEmail()
+  email: string
+
+  @IsNotEmpty()
+  verifyCode: string
+
+  @IsNotEmpty()
+  token: string
+
+  @IsNotEmpty()
+  @IsStrongPassword()
+  password: string
+}
diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts
index 8c13848..9ebce58 100644
--- a/src/users/users.controller.ts
+++ b/src/users/users.controller.ts
@@ -1,4 +1,11 @@
-import { Controller, Get, Post, Body, UseInterceptors } from '@nestjs/common'
+import {
+  Controller,
+  Get,
+  Post,
+  Patch,
+  Body,
+  UseInterceptors,
+} from '@nestjs/common'
 import { UsersService } from './users.service'
 import { ApiTags, ApiOperation } from '@nestjs/swagger'
 import { User } from 'src/common/decorators/user.decorator'
@@ -7,6 +14,7 @@ import { PasswordInterceptor } from 'src/common/interceptors/password.intercepto
 import { PrismaService } from 'nestjs-prisma'
 import { UserEntity } from './entities/user.entity'
 import { CreateUserDto } from './dto/create-user.dto'
+import { UpdatePassword } from './dto/update-password.dto'
 
 @ApiTags('User')
 @Controller('api/users')
@@ -29,4 +37,17 @@ export class UsersController {
   async register(@Body() userData: CreateUserDto) {
     return this.userService.register(userData)
   }
+
+  @ApiOperation({ summary: '修改密码' })
+  @UseInterceptors(PasswordInterceptor)
+  @Patch('me/password')
+  async updatePassord(@Body() payload: UpdatePassword): Promise<UserEntity> {
+    return this.userService.updatePassword(payload)
+  }
+
+  // @ApiOperation({ summary: '修改邮箱' })
+  // @Patch('me/email')
+  // async updateEmail(@Body() payload: unknown) {
+  //   return
+  // }
 }
diff --git a/src/users/users.service.ts b/src/users/users.service.ts
index 1d41738..3a85ff8 100644
--- a/src/users/users.service.ts
+++ b/src/users/users.service.ts
@@ -6,9 +6,8 @@ import { securityConfig, SecurityConfig } from 'src/common/configs'
 import { CreateUserDto } from 'src/users/dto/create-user.dto'
 import { EmailSendDto, EmailScene } from 'src/email/dto/email.dto'
 import { EmailService } from 'src/email/email.service'
-import { Prisma } from '@prisma/client'
 import { TokenService } from './token.service'
-
+import { UpdatePassword } from './dto/update-password.dto'
 @Injectable()
 export class UsersService {
   constructor(
@@ -24,27 +23,9 @@ export class UsersService {
     await this.verifyEmail(
       userToCreate.email,
       userToCreate.token,
-      userToCreate.verificationCode,
+      userToCreate.verifyCode,
       EmailScene.register,
     )
-    return this.createUser(userToCreate)
-  }
-
-  private async verifyEmail(
-    email: string,
-    token: string,
-    verificationCode: string,
-    scene: EmailScene,
-  ) {
-    const payload = this.jwtService.verify<EmailSendDto>(token, {
-      secret: this.emailService.getEmailJwtSecret(verificationCode, scene),
-    })
-    if (payload.email !== email || payload.scene !== scene) {
-      throw new ForbiddenException('请输入正确的邮箱验证码')
-    }
-  }
-
-  private async createUser(userToCreate: Prisma.UserCreateInput) {
     const hashedPassword = await bcrypt.hash(
       userToCreate.password,
       this.secureConfig.bcryptSaltOrRound,
@@ -58,4 +39,36 @@ export class UsersService {
     })
     return this.tokenService.generateTokens({ userId: user.id })
   }
+
+  async updatePassword(payload: UpdatePassword) {
+    await this.verifyEmail(
+      payload.email,
+      payload.token,
+      payload.verifyCode,
+      EmailScene.updatePassword,
+    )
+    const hashedPassword = await bcrypt.hash(
+      payload.password,
+      this.secureConfig.bcryptSaltOrRound,
+    )
+    const user = await this.prismaService.user.update({
+      where: { email: payload.email },
+      data: { password: hashedPassword },
+    })
+    return user
+  }
+
+  private async verifyEmail(
+    email: string,
+    token: string,
+    verifyCode: string,
+    scene: EmailScene,
+  ) {
+    const payload = this.jwtService.verify<EmailSendDto>(token, {
+      secret: this.emailService.getEmailJwtSecret(verifyCode, scene),
+    })
+    if (payload.email !== email || payload.scene !== scene) {
+      throw new ForbiddenException('请输入正确的邮箱验证码')
+    }
+  }
 }