refactor: controllers
This commit is contained in:
		
							parent
							
								
									d3c8626305
								
							
						
					
					
						commit
						a863806111
					
				| @ -1,14 +1,15 @@ | |||||||
| import { | import { | ||||||
|   ConflictException, |  | ||||||
|   Inject, |   Inject, | ||||||
|   Injectable, |   Injectable, | ||||||
|  |   ConflictException, | ||||||
|  |   ForbiddenException, | ||||||
|   NotFoundException, |   NotFoundException, | ||||||
| } from '@nestjs/common' | } from '@nestjs/common' | ||||||
| import { securityConfig, SecurityConfig } from 'src/common/configs' | import { securityConfig, SecurityConfig } from 'src/common/configs' | ||||||
| import { MailerService } from '@nestjs-modules/mailer' | import { MailerService } from '@nestjs-modules/mailer' | ||||||
| import { JwtService } from '@nestjs/jwt' | import { JwtService } from '@nestjs/jwt' | ||||||
| import { EmailScene } from './dto/email.dto' |  | ||||||
| import { PrismaService } from 'nestjs-prisma' | import { PrismaService } from 'nestjs-prisma' | ||||||
|  | import { EmailSendDto, EmailScene } from 'src/email/dto/email.dto' | ||||||
| 
 | 
 | ||||||
| @Injectable() | @Injectable() | ||||||
| export class EmailService { | export class EmailService { | ||||||
| @ -55,10 +56,26 @@ export class EmailService { | |||||||
|       subject: `【qiuxu.site】${this.subjectMap[scene]}`, |       subject: `【qiuxu.site】${this.subjectMap[scene]}`, | ||||||
|       html: `您正在qiuxu.site${this.subjectMap[scene]},验证码为 <strong>${verifyCode}</strong>,30分钟内有效`, |       html: `您正在qiuxu.site${this.subjectMap[scene]},验证码为 <strong>${verifyCode}</strong>,30分钟内有效`, | ||||||
|     }) |     }) | ||||||
|     return registerToken |     return { registerToken, userId: user.id } | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   getEmailJwtSecret(verifyCode: string, scene: EmailScene) { |   // TODO: 做成守卫?
 | ||||||
|  |   async verifyEmail(data: { | ||||||
|  |     email: string | ||||||
|  |     token: string | ||||||
|  |     verifyCode: string | ||||||
|  |     scene: EmailScene | ||||||
|  |   }) { | ||||||
|  |     const { email, token, verifyCode, scene } = data | ||||||
|  |     const payload = this.jwtService.verify<EmailSendDto>(token, { | ||||||
|  |       secret: this.getEmailJwtSecret(verifyCode, scene), | ||||||
|  |     }) | ||||||
|  |     if (payload.email !== email || payload.scene !== scene) { | ||||||
|  |       throw new ForbiddenException('请输入正确的邮箱验证码') | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private getEmailJwtSecret(verifyCode: string, scene: EmailScene) { | ||||||
|     return this.secureConfig.jwt_access_secret + verifyCode + scene |     return this.secureConfig.jwt_access_secret + verifyCode + scene | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|  | |||||||
| @ -1,6 +1,6 @@ | |||||||
| import { IsEmail, IsNotEmpty, IsStrongPassword } from 'class-validator' | import { IsEmail, IsNotEmpty, IsStrongPassword } from 'class-validator' | ||||||
| 
 | 
 | ||||||
| export class ForgetPassword { | export class ResetPassword { | ||||||
|   @IsNotEmpty() |   @IsNotEmpty() | ||||||
|   @IsEmail() |   @IsEmail() | ||||||
|   email: string |   email: string | ||||||
							
								
								
									
										93
									
								
								src/users/me.controller.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										93
									
								
								src/users/me.controller.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,93 @@ | |||||||
|  | import { | ||||||
|  |   Controller, | ||||||
|  |   Get, | ||||||
|  |   Delete, | ||||||
|  |   Patch, | ||||||
|  |   Put, | ||||||
|  |   Body, | ||||||
|  |   UseInterceptors, | ||||||
|  |   BadRequestException, | ||||||
|  | } from '@nestjs/common' | ||||||
|  | import { MeService } from './me.service' | ||||||
|  | import { ApiTags, ApiOperation, ApiUnauthorizedResponse } from '@nestjs/swagger' | ||||||
|  | import { User } from 'src/common/decorators/user.decorator' | ||||||
|  | import { NeedAuth } from 'src/common/decorators/need-auth.decorator' | ||||||
|  | import { PasswordInterceptor } from 'src/common/interceptors/password.interceptor' | ||||||
|  | import { PrismaService } from 'nestjs-prisma' | ||||||
|  | import { UserEntity } from './entities/user.entity' | ||||||
|  | import { DeleteUserDto } from './dto/delete-user.dto' | ||||||
|  | import { ChangePassword } from './dto/change-password.dto' | ||||||
|  | import { UpdateUserDto } from './dto/update-user.dto' | ||||||
|  | import { TokenRefreshPayload } from './dto/token.dto' | ||||||
|  | 
 | ||||||
|  | @Controller('api/users/me') | ||||||
|  | @ApiTags('Me') | ||||||
|  | export class MeController { | ||||||
|  |   constructor( | ||||||
|  |     private readonly meService: MeService, | ||||||
|  |     private readonly prismaService: PrismaService, | ||||||
|  |   ) {} | ||||||
|  | 
 | ||||||
|  |   @Get() | ||||||
|  |   @ApiOperation({ summary: '获取用户信息' }) | ||||||
|  |   @UseInterceptors(PasswordInterceptor) | ||||||
|  |   @NeedAuth() | ||||||
|  |   async getUserInfo(@User('userId') userId: string): Promise<UserEntity> { | ||||||
|  |     return this.prismaService.user.findUniqueOrThrow({ | ||||||
|  |       where: { id: userId }, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Patch() | ||||||
|  |   @ApiOperation({ summary: '修改用户信息(用户名等)' }) | ||||||
|  |   @UseInterceptors(PasswordInterceptor) | ||||||
|  |   @NeedAuth() | ||||||
|  |   async updateUserInfo( | ||||||
|  |     @Body() payload: UpdateUserDto, | ||||||
|  |     @User('userId') userId: string, | ||||||
|  |   ): Promise<UserEntity> { | ||||||
|  |     if (Object.keys(payload).length === 0) { | ||||||
|  |       throw new BadRequestException() | ||||||
|  |     } | ||||||
|  |     return this.prismaService.user.update({ | ||||||
|  |       where: { id: userId }, | ||||||
|  |       data: payload, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Delete() | ||||||
|  |   @NeedAuth() | ||||||
|  |   @ApiOperation({ summary: '删除用户' }) | ||||||
|  |   async deleteUser( | ||||||
|  |     @Body() userData: DeleteUserDto, | ||||||
|  |     @User('userId') userId: string, | ||||||
|  |   ) { | ||||||
|  |     return this.meService.deleteUser(userData, userId) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Patch('?field=password') | ||||||
|  |   @NeedAuth() | ||||||
|  |   @ApiOperation({ summary: '修改密码' }) | ||||||
|  |   @UseInterceptors(PasswordInterceptor) | ||||||
|  |   async changePassword( | ||||||
|  |     @Body() payload: ChangePassword, | ||||||
|  |     @User('userId') userId: string, | ||||||
|  |   ) { | ||||||
|  |     return this.meService.changePassword(payload, userId) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Patch('?field=email') | ||||||
|  |   @NeedAuth() | ||||||
|  |   @ApiOperation({ summary: '修改邮箱(TODO)' }) | ||||||
|  |   @UseInterceptors(PasswordInterceptor) | ||||||
|  |   async updateEmail(@Body() payload: unknown) { | ||||||
|  |     return '修改邮箱' | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   @Put('token') | ||||||
|  |   @ApiOperation({ summary: '刷新token' }) | ||||||
|  |   @ApiUnauthorizedResponse({ description: 'Unauthorized' }) | ||||||
|  |   async updateAccessToken(@Body() payload: TokenRefreshPayload) { | ||||||
|  |     return this.meService.updateAccessToken(payload.refreshToken) | ||||||
|  |   } | ||||||
|  | } | ||||||
							
								
								
									
										95
									
								
								src/users/me.service.ts
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										95
									
								
								src/users/me.service.ts
									
									
									
									
									
										Normal file
									
								
							| @ -0,0 +1,95 @@ | |||||||
|  | import { | ||||||
|  |   Inject, | ||||||
|  |   Injectable, | ||||||
|  |   ForbiddenException, | ||||||
|  |   UnauthorizedException, | ||||||
|  | } from '@nestjs/common' | ||||||
|  | import * as bcrypt from 'bcrypt' | ||||||
|  | import { PrismaService } from 'nestjs-prisma' | ||||||
|  | import { JwtService } from '@nestjs/jwt' | ||||||
|  | import { securityConfig, SecurityConfig } from 'src/common/configs' | ||||||
|  | import { EmailScene } from 'src/email/dto/email.dto' | ||||||
|  | import { EmailService } from 'src/email/email.service' | ||||||
|  | import { DeleteUserDto } from './dto/delete-user.dto' | ||||||
|  | import { ChangePassword } from './dto/change-password.dto' | ||||||
|  | import { TokenContnet } from './dto/token.dto' | ||||||
|  | 
 | ||||||
|  | @Injectable() | ||||||
|  | export class MeService { | ||||||
|  |   constructor( | ||||||
|  |     private jwtService: JwtService, | ||||||
|  |     private prismaService: PrismaService, | ||||||
|  |     private emailService: EmailService, | ||||||
|  |     @Inject(securityConfig.KEY) private secureConfig: SecurityConfig, | ||||||
|  |   ) {} | ||||||
|  | 
 | ||||||
|  |   async deleteUser(userToDelete: DeleteUserDto, userId: string) { | ||||||
|  |     const { email, token, verifyCode, password } = userToDelete | ||||||
|  |     await this.emailService.verifyEmail({ | ||||||
|  |       email, | ||||||
|  |       token, | ||||||
|  |       verifyCode, | ||||||
|  |       scene: EmailScene.deleteUser, | ||||||
|  |     }) | ||||||
|  |     const user = await this.prismaService.user.findUniqueOrThrow({ | ||||||
|  |       where: { email: userToDelete.email }, | ||||||
|  |     }) | ||||||
|  |     if (user.id !== userId) { | ||||||
|  |       throw new ForbiddenException() | ||||||
|  |     } | ||||||
|  |     const passwordValid = await bcrypt.compare(password, user.password) | ||||||
|  |     if (!passwordValid) { | ||||||
|  |       throw new ForbiddenException('Invalid password') | ||||||
|  |     } | ||||||
|  | 
 | ||||||
|  |     return this.prismaService.user.delete({ | ||||||
|  |       where: { email: userToDelete.email }, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async changePassword(payload: ChangePassword, userId: string) { | ||||||
|  |     const user = await this.prismaService.user.findUniqueOrThrow({ | ||||||
|  |       where: { id: userId }, | ||||||
|  |     }) | ||||||
|  |     await this.checkPassword(payload.oldPassword, user.password) | ||||||
|  |     const hashedPassword = await bcrypt.hash( | ||||||
|  |       payload.newPassword, | ||||||
|  |       this.secureConfig.bcryptSaltOrRound, | ||||||
|  |     ) | ||||||
|  |     return this.prismaService.user.update({ | ||||||
|  |       where: { id: userId }, | ||||||
|  |       data: { password: hashedPassword }, | ||||||
|  |     }) | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   async updateAccessToken(refreshToken: string) { | ||||||
|  |     const { userId, iat } = this.jwtService.verify<TokenContnet>(refreshToken, { | ||||||
|  |       secret: this.secureConfig.jwt_refresh_secret, | ||||||
|  |     }) | ||||||
|  |     const user = await this.prismaService.user.findUniqueOrThrow({ | ||||||
|  |       where: { id: userId }, | ||||||
|  |     }) | ||||||
|  |     // TODO:不使用updatedAt,而是自定义的一个refreshTime字段
 | ||||||
|  |     if (iat * 1000 < user.updatedAt.getTime()) { | ||||||
|  |       throw new UnauthorizedException('token失效,请重新登录') | ||||||
|  |     } | ||||||
|  |     return { | ||||||
|  |       userId, | ||||||
|  |       refreshToken: refreshToken, | ||||||
|  |       accessToken: this.jwtService.sign( | ||||||
|  |         { userId }, | ||||||
|  |         { | ||||||
|  |           secret: this.secureConfig.jwt_refresh_secret, | ||||||
|  |           expiresIn: this.secureConfig.refreshIn, | ||||||
|  |         }, | ||||||
|  |       ), | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | 
 | ||||||
|  |   private async checkPassword(pwd: string, hashPwd: string) { | ||||||
|  |     const valid = await bcrypt.compare(pwd, hashPwd) | ||||||
|  |     if (!valid) { | ||||||
|  |       throw new ForbiddenException('Invalid password') | ||||||
|  |     } | ||||||
|  |   } | ||||||
|  | } | ||||||
| @ -1,25 +0,0 @@ | |||||||
| import { Controller, Post, Put, Body } from '@nestjs/common' |  | ||||||
| import { ApiTags, ApiOperation, ApiUnauthorizedResponse } from '@nestjs/swagger' |  | ||||||
| import { LoginInputDto } from './dto/login-input.dto' |  | ||||||
| import { TokenRefreshPayload } from './dto/token.dto' |  | ||||||
| import { TokenService } from './token.service' |  | ||||||
| 
 |  | ||||||
| @ApiTags('token') |  | ||||||
| @Controller('api/token') |  | ||||||
| export class TokenController { |  | ||||||
|   constructor(private tokenService: TokenService) {} |  | ||||||
| 
 |  | ||||||
|   // TODO: 限制调用频率,避免暴力破解
 |  | ||||||
|   @ApiOperation({ summary: '登录用户' }) |  | ||||||
|   @Post() |  | ||||||
|   async login(@Body() user: LoginInputDto) { |  | ||||||
|     return this.tokenService.login(user.email, user.password) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @ApiOperation({ summary: '刷新token' }) |  | ||||||
|   @ApiUnauthorizedResponse({ description: 'Unauthorized' }) |  | ||||||
|   @Put() |  | ||||||
|   async refreshToken(@Body() payload: TokenRefreshPayload) { |  | ||||||
|     return this.tokenService.refreshToken(payload.refreshToken) |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,65 +0,0 @@ | |||||||
| import { Inject, Injectable, UnauthorizedException } from '@nestjs/common' |  | ||||||
| import * as bcrypt from 'bcrypt' |  | ||||||
| import { PrismaService } from 'nestjs-prisma' |  | ||||||
| import { Token, TokenPayload, TokenContnet } from './dto/token.dto' |  | ||||||
| import { JwtService } from '@nestjs/jwt' |  | ||||||
| import { securityConfig, SecurityConfig } from 'src/common/configs' |  | ||||||
| 
 |  | ||||||
| @Injectable() |  | ||||||
| export class TokenService { |  | ||||||
|   constructor( |  | ||||||
|     private jwtService: JwtService, |  | ||||||
|     private prismaService: PrismaService, |  | ||||||
|     @Inject(securityConfig.KEY) |  | ||||||
|     private secureConfig: SecurityConfig, |  | ||||||
|   ) {} |  | ||||||
| 
 |  | ||||||
|   async login(email: string, password: string) { |  | ||||||
|     const user = await this.prismaService.user.findUniqueOrThrow({ |  | ||||||
|       where: { email }, |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     const passwordValid = await bcrypt.compare(password, user.password) |  | ||||||
| 
 |  | ||||||
|     if (!passwordValid) { |  | ||||||
|       throw new UnauthorizedException('Invalid password') |  | ||||||
|     } |  | ||||||
| 
 |  | ||||||
|     return this.generateTokens({ userId: user.id }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async refreshToken(token: string) { |  | ||||||
|     const { userId, iat } = this.jwtService.verify<TokenContnet>(token, { |  | ||||||
|       secret: this.secureConfig.jwt_refresh_secret, |  | ||||||
|     }) |  | ||||||
|     const user = await this.prismaService.user.findUniqueOrThrow({ |  | ||||||
|       where: { id: userId }, |  | ||||||
|     }) |  | ||||||
|     if (iat * 1000 < user.updatedAt.getTime()) { |  | ||||||
|       throw new UnauthorizedException('token失效,请重新登录') |  | ||||||
|     } |  | ||||||
|     return { |  | ||||||
|       refreshToken: token, |  | ||||||
|       accessToken: this.jwtService.sign( |  | ||||||
|         { userId }, |  | ||||||
|         { |  | ||||||
|           secret: this.secureConfig.jwt_refresh_secret, |  | ||||||
|           expiresIn: this.secureConfig.refreshIn, |  | ||||||
|         }, |  | ||||||
|       ), |  | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   generateTokens(payload: TokenPayload): Token { |  | ||||||
|     const accessToken = this.jwtService.sign(payload, { |  | ||||||
|       secret: this.secureConfig.jwt_access_secret, |  | ||||||
|       expiresIn: this.secureConfig.expiresIn, |  | ||||||
|     }) |  | ||||||
|     const refreshToken = this.jwtService.sign(payload, { |  | ||||||
|       secret: this.secureConfig.jwt_refresh_secret, |  | ||||||
|       expiresIn: this.secureConfig.refreshIn, |  | ||||||
|     }) |  | ||||||
| 
 |  | ||||||
|     return { accessToken, refreshToken } |  | ||||||
|   } |  | ||||||
| } |  | ||||||
| @ -1,105 +1,34 @@ | |||||||
| import { | import { Controller, Post, Patch, Body, Param } from '@nestjs/common' | ||||||
|   Controller, |  | ||||||
|   Get, |  | ||||||
|   Post, |  | ||||||
|   Delete, |  | ||||||
|   Put, |  | ||||||
|   Patch, |  | ||||||
|   Body, |  | ||||||
|   UseInterceptors, |  | ||||||
|   BadRequestException, |  | ||||||
| } from '@nestjs/common' |  | ||||||
| import { UsersService } from './users.service' | import { UsersService } from './users.service' | ||||||
| import { ApiTags, ApiOperation } from '@nestjs/swagger' | import { ApiTags, ApiOperation } from '@nestjs/swagger' | ||||||
| import { User } from 'src/common/decorators/user.decorator' |  | ||||||
| import { NeedAuth } from 'src/common/decorators/need-auth.decorator' |  | ||||||
| import { PasswordInterceptor } from 'src/common/interceptors/password.interceptor' |  | ||||||
| import { PrismaService } from 'nestjs-prisma' |  | ||||||
| import { UserEntity } from './entities/user.entity' |  | ||||||
| import { CreateUserDto } from './dto/create-user.dto' | import { CreateUserDto } from './dto/create-user.dto' | ||||||
| import { ForgetPassword } from './dto/forget-password.dto' | import { LoginInputDto } from './dto/login-input.dto' | ||||||
| import { DeleteUserDto } from './dto/delete-user.dto' | import { ResetPassword } from './dto/reset-password.dto' | ||||||
| import { ChangePassword } from './dto/change-password.dto' |  | ||||||
| import { UpdateUserDto } from './dto/update-user.dto' |  | ||||||
| 
 | 
 | ||||||
| @Controller('api/users') | @Controller('api/users') | ||||||
|  | @ApiTags('Users') | ||||||
| export class UsersController { | export class UsersController { | ||||||
|   constructor( |   constructor(private readonly usersService: UsersService) {} | ||||||
|     private readonly userService: UsersService, |  | ||||||
|     private readonly prismaService: PrismaService, |  | ||||||
|   ) {} |  | ||||||
| 
 | 
 | ||||||
|   @ApiTags('Me') |  | ||||||
|   @ApiOperation({ summary: '获取用户信息' }) |  | ||||||
|   @UseInterceptors(PasswordInterceptor) |  | ||||||
|   @NeedAuth() |  | ||||||
|   @Get('me') |  | ||||||
|   async getUserInfo(@User('userId') userId: string): Promise<UserEntity> { |  | ||||||
|     return this.prismaService.user.findUniqueOrThrow({ where: { id: userId } }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @ApiTags('Me') |  | ||||||
|   @ApiOperation({ summary: '修改用户信息(用户名等)' }) |  | ||||||
|   @UseInterceptors(PasswordInterceptor) |  | ||||||
|   @NeedAuth() |  | ||||||
|   @Patch('me') |  | ||||||
|   async updateUserInfo( |  | ||||||
|     @Body() payload: UpdateUserDto, |  | ||||||
|     @User('userId') userId: string, |  | ||||||
|   ): Promise<UserEntity> { |  | ||||||
|     if (Object.keys(payload).length === 0) { |  | ||||||
|       throw new BadRequestException() |  | ||||||
|     } |  | ||||||
|     return this.prismaService.user.update({ |  | ||||||
|       where: { id: userId }, |  | ||||||
|       data: payload, |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @ApiTags('User') |  | ||||||
|   @ApiOperation({ summary: '注册用户' }) |  | ||||||
|   @Post() |   @Post() | ||||||
|   async register(@Body() userData: CreateUserDto) { |   @ApiOperation({ summary: '邮箱注册' }) | ||||||
|     return this.userService.register(userData) |   async registerByEmail(@Body() userData: CreateUserDto) { | ||||||
|  |     return this.usersService.registerByEmail(userData) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   @ApiTags('Me') |   // TODO: 限制调用频率,避免暴力破解
 | ||||||
|   @NeedAuth() |   @Post('token') | ||||||
|   @ApiOperation({ summary: '删除用户' }) |   @ApiOperation({ summary: '邮箱登录' }) | ||||||
|   @Delete('me') |   async loginByEmail(@Body() user: LoginInputDto) { | ||||||
|   async deleteUser( |     return this.usersService.loginByEmail(user.email, user.password) | ||||||
|     @Body() userData: DeleteUserDto, |   } | ||||||
|     @User('userId') userId: string, | 
 | ||||||
|  |   @Patch(':id/?field=password') | ||||||
|  |   @ApiOperation({ summary: '找回密码' }) | ||||||
|  |   async forgetPassword( | ||||||
|  |     @Body() payload: ResetPassword, | ||||||
|  |     @Param('id') userId: string, | ||||||
|   ) { |   ) { | ||||||
|     return this.userService.deleteUser(userData, userId) |     return this.usersService.resetPasswordByEmail(payload, userId) | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @ApiTags('Me') |  | ||||||
|   @NeedAuth() |  | ||||||
|   @ApiOperation({ summary: '修改密码' }) |  | ||||||
|   @UseInterceptors(PasswordInterceptor) |  | ||||||
|   @Patch('me/password') |  | ||||||
|   async changePassword( |  | ||||||
|     @Body() payload: ChangePassword, |  | ||||||
|     @User('userId') userId: string, |  | ||||||
|   ) { |  | ||||||
|     return this.userService.changePasswor(payload, userId) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @ApiTags('User') |  | ||||||
|   @ApiOperation({ summary: '忘记密码' }) |  | ||||||
|   @UseInterceptors(PasswordInterceptor) |  | ||||||
|   @Patch('password') |  | ||||||
|   async forgetPassword(@Body() payload: ForgetPassword): Promise<UserEntity> { |  | ||||||
|     return this.userService.forgetPassword(payload) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   @ApiTags('Me') |  | ||||||
|   @NeedAuth() |  | ||||||
|   @ApiOperation({ summary: '修改邮箱(TODO)' }) |  | ||||||
|   @UseInterceptors(PasswordInterceptor) |  | ||||||
|   @Patch('me/email') |  | ||||||
|   async updateEmail(@Body() payload: unknown) { |  | ||||||
|     return '修改邮箱' |  | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
| @ -3,12 +3,12 @@ import { UsersService } from './users.service' | |||||||
| import { UsersController } from './users.controller' | import { UsersController } from './users.controller' | ||||||
| import { JwtService } from '@nestjs/jwt' | import { JwtService } from '@nestjs/jwt' | ||||||
| import { EmailService } from 'src/email/email.service' | import { EmailService } from 'src/email/email.service' | ||||||
| import { TokenController } from './token.controller' | import { MeService } from './me.service' | ||||||
| import { TokenService } from './token.service' | import { MeController } from './me.controller' | ||||||
| 
 | 
 | ||||||
| @Module({ | @Module({ | ||||||
|   controllers: [UsersController, TokenController], |   controllers: [UsersController, MeController], | ||||||
|   providers: [UsersService, JwtService, EmailService, TokenService], |   providers: [UsersService, JwtService, EmailService, MeService], | ||||||
|   exports: [UsersService], |   exports: [UsersService], | ||||||
| }) | }) | ||||||
| export class UsersModule {} | export class UsersModule {} | ||||||
|  | |||||||
| @ -1,15 +1,13 @@ | |||||||
| import { Inject, Injectable, ForbiddenException } from '@nestjs/common' | import { Inject, Injectable, UnauthorizedException } from '@nestjs/common' | ||||||
| import * as bcrypt from 'bcrypt' | import * as bcrypt from 'bcrypt' | ||||||
| import { PrismaService } from 'nestjs-prisma' | import { PrismaService } from 'nestjs-prisma' | ||||||
| import { JwtService } from '@nestjs/jwt' | import { JwtService } from '@nestjs/jwt' | ||||||
| import { securityConfig, SecurityConfig } from 'src/common/configs' | import { securityConfig, type SecurityConfig } from 'src/common/configs' | ||||||
| import { CreateUserDto } from 'src/users/dto/create-user.dto' | import { CreateUserDto } from 'src/users/dto/create-user.dto' | ||||||
| import { EmailSendDto, EmailScene } from 'src/email/dto/email.dto' | import { EmailScene } from 'src/email/dto/email.dto' | ||||||
| import { EmailService } from 'src/email/email.service' | import { EmailService } from 'src/email/email.service' | ||||||
| import { TokenService } from './token.service' | import { ResetPassword } from './dto/reset-password.dto' | ||||||
| import { ForgetPassword } from './dto/forget-password.dto' | import { Token, TokenPayload } from './dto/token.dto' | ||||||
| import { DeleteUserDto } from './dto/delete-user.dto' |  | ||||||
| import { ChangePassword } from './dto/change-password.dto' |  | ||||||
| 
 | 
 | ||||||
| @Injectable() | @Injectable() | ||||||
| export class UsersService { | export class UsersService { | ||||||
| @ -17,109 +15,70 @@ export class UsersService { | |||||||
|     private jwtService: JwtService, |     private jwtService: JwtService, | ||||||
|     private prismaService: PrismaService, |     private prismaService: PrismaService, | ||||||
|     private emailService: EmailService, |     private emailService: EmailService, | ||||||
|     private tokenService: TokenService, |     @Inject(securityConfig.KEY) private secureConfig: SecurityConfig, | ||||||
|     @Inject(securityConfig.KEY) |  | ||||||
|     private secureConfig: SecurityConfig, |  | ||||||
|   ) {} |   ) {} | ||||||
| 
 | 
 | ||||||
|   async register(userToCreate: CreateUserDto) { |   async registerByEmail(userToCreate: CreateUserDto) { | ||||||
|     await this.verifyEmail( |     const { email, token, verifyCode, username, password } = userToCreate | ||||||
|       userToCreate.email, |     await this.emailService.verifyEmail({ | ||||||
|       userToCreate.token, |       email, | ||||||
|       userToCreate.verifyCode, |       token, | ||||||
|       EmailScene.register, |       verifyCode, | ||||||
|     ) |       scene: EmailScene.register, | ||||||
|  |     }) | ||||||
|     const hashedPassword = await bcrypt.hash( |     const hashedPassword = await bcrypt.hash( | ||||||
|       userToCreate.password, |       password, | ||||||
|       this.secureConfig.bcryptSaltOrRound, |       this.secureConfig.bcryptSaltOrRound, | ||||||
|     ) |     ) | ||||||
|     const user = await this.prismaService.user.create({ |     const user = await this.prismaService.user.create({ | ||||||
|       data: { |       data: { username, email, password: hashedPassword }, | ||||||
|         username: userToCreate.username, |  | ||||||
|         email: userToCreate.email, |  | ||||||
|         password: hashedPassword, |  | ||||||
|       }, |  | ||||||
|     }) |     }) | ||||||
|     return this.tokenService.generateTokens({ userId: user.id }) |     return this.generateTokens({ userId: user.id }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async deleteUser(userToDelete: DeleteUserDto, userId: string) { |   async loginByEmail(email: string, password: string) { | ||||||
|     await this.verifyEmail( |  | ||||||
|       userToDelete.email, |  | ||||||
|       userToDelete.token, |  | ||||||
|       userToDelete.verifyCode, |  | ||||||
|       EmailScene.deleteUser, |  | ||||||
|     ) |  | ||||||
|     const user = await this.prismaService.user.findUniqueOrThrow({ |     const user = await this.prismaService.user.findUniqueOrThrow({ | ||||||
|       where: { email: userToDelete.email }, |       where: { email }, | ||||||
|     }) |     }) | ||||||
|     if (user.id !== userId) { | 
 | ||||||
|       throw new ForbiddenException() |     const passwordValid = await bcrypt.compare(password, user.password) | ||||||
|     } | 
 | ||||||
|     const passwordValid = await bcrypt.compare( |  | ||||||
|       user.password, |  | ||||||
|       userToDelete.password, |  | ||||||
|     ) |  | ||||||
|     if (!passwordValid) { |     if (!passwordValid) { | ||||||
|       throw new ForbiddenException('Invalid password') |       throw new UnauthorizedException('Invalid password') | ||||||
|     } |     } | ||||||
| 
 | 
 | ||||||
|     return this.prismaService.user.delete({ |     return this.generateTokens({ userId: user.id }) | ||||||
|       where: { email: userToDelete.email }, |  | ||||||
|     }) |  | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   async changePasswor(payload: ChangePassword, userId: string) { |   async resetPasswordByEmail(data: ResetPassword, userId: string) { | ||||||
|     const user = await this.prismaService.user.findFirstOrThrow({ |     const { email, token, verifyCode, password } = data | ||||||
|       where: { id: userId }, |     await this.emailService.verifyEmail({ | ||||||
|  |       email, | ||||||
|  |       token, | ||||||
|  |       verifyCode, | ||||||
|  |       scene: EmailScene.forgetPassword, | ||||||
|     }) |     }) | ||||||
|     await this.checkPassword(payload.oldPassword, user.password) |  | ||||||
|     const hashedPassword = await bcrypt.hash( |     const hashedPassword = await bcrypt.hash( | ||||||
|       payload.newPassword, |       password, | ||||||
|       this.secureConfig.bcryptSaltOrRound, |  | ||||||
|     ) |  | ||||||
|     return this.prismaService.user.update({ |  | ||||||
|       where: { id: userId }, |  | ||||||
|       data: { password: hashedPassword }, |  | ||||||
|     }) |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   async forgetPassword(payload: ForgetPassword) { |  | ||||||
|     await this.verifyEmail( |  | ||||||
|       payload.email, |  | ||||||
|       payload.token, |  | ||||||
|       payload.verifyCode, |  | ||||||
|       EmailScene.forgetPassword, |  | ||||||
|     ) |  | ||||||
|     const hashedPassword = await bcrypt.hash( |  | ||||||
|       payload.password, |  | ||||||
|       this.secureConfig.bcryptSaltOrRound, |       this.secureConfig.bcryptSaltOrRound, | ||||||
|     ) |     ) | ||||||
|     const user = await this.prismaService.user.update({ |     const user = await this.prismaService.user.update({ | ||||||
|       where: { email: payload.email }, |       where: { id: userId }, | ||||||
|       data: { password: hashedPassword }, |       data: { password: hashedPassword }, | ||||||
|     }) |     }) | ||||||
|     return user |     return this.generateTokens({ userId: user.id }) | ||||||
|   } |   } | ||||||
| 
 | 
 | ||||||
|   private async checkPassword(pwd: string, hashPwd: string) { |   private generateTokens(payload: TokenPayload): Token { | ||||||
|     const valid = await bcrypt.compare(pwd, hashPwd) |     const accessToken = this.jwtService.sign(payload, { | ||||||
|     if (!valid) { |       secret: this.secureConfig.jwt_access_secret, | ||||||
|       throw new ForbiddenException('Invalid password') |       expiresIn: this.secureConfig.expiresIn, | ||||||
|     } |  | ||||||
|   } |  | ||||||
| 
 |  | ||||||
|   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) { |     const refreshToken = this.jwtService.sign(payload, { | ||||||
|       throw new ForbiddenException('请输入正确的邮箱验证码') |       secret: this.secureConfig.jwt_refresh_secret, | ||||||
|     } |       expiresIn: this.secureConfig.refreshIn, | ||||||
|  |     }) | ||||||
|  | 
 | ||||||
|  |     return { accessToken, refreshToken, ...payload } | ||||||
|   } |   } | ||||||
| } | } | ||||||
|  | |||||||
		Loading…
	
		Reference in New Issue
	
	Block a user