refactor: controllers

This commit is contained in:
秦秋旭 2023-02-23 17:09:41 +08:00
parent d3c8626305
commit a863806111
9 changed files with 279 additions and 276 deletions

View File

@ -1,14 +1,15 @@
import {
ConflictException,
Inject,
Injectable,
ConflictException,
ForbiddenException,
NotFoundException,
} from '@nestjs/common'
import { securityConfig, SecurityConfig } from 'src/common/configs'
import { MailerService } from '@nestjs-modules/mailer'
import { JwtService } from '@nestjs/jwt'
import { EmailScene } from './dto/email.dto'
import { PrismaService } from 'nestjs-prisma'
import { EmailSendDto, EmailScene } from 'src/email/dto/email.dto'
@Injectable()
export class EmailService {
@ -55,10 +56,26 @@ export class EmailService {
subject: `【qiuxu.site】${this.subjectMap[scene]}`,
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
}

View File

@ -1,6 +1,6 @@
import { IsEmail, IsNotEmpty, IsStrongPassword } from 'class-validator'
export class ForgetPassword {
export class ResetPassword {
@IsNotEmpty()
@IsEmail()
email: string

View 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
View 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')
}
}
}

View File

@ -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)
}
}

View File

@ -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 }
}
}

View File

@ -1,105 +1,34 @@
import {
Controller,
Get,
Post,
Delete,
Put,
Patch,
Body,
UseInterceptors,
BadRequestException,
} from '@nestjs/common'
import { Controller, Post, Patch, Body, Param } from '@nestjs/common'
import { UsersService } from './users.service'
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 { ForgetPassword } from './dto/forget-password.dto'
import { DeleteUserDto } from './dto/delete-user.dto'
import { ChangePassword } from './dto/change-password.dto'
import { UpdateUserDto } from './dto/update-user.dto'
import { LoginInputDto } from './dto/login-input.dto'
import { ResetPassword } from './dto/reset-password.dto'
@Controller('api/users')
@ApiTags('Users')
export class UsersController {
constructor(
private readonly userService: UsersService,
private readonly prismaService: PrismaService,
) {}
constructor(private readonly usersService: UsersService) {}
@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()
async register(@Body() userData: CreateUserDto) {
return this.userService.register(userData)
@ApiOperation({ summary: '邮箱注册' })
async registerByEmail(@Body() userData: CreateUserDto) {
return this.usersService.registerByEmail(userData)
}
@ApiTags('Me')
@NeedAuth()
@ApiOperation({ summary: '删除用户' })
@Delete('me')
async deleteUser(
@Body() userData: DeleteUserDto,
@User('userId') userId: string,
// TODO: 限制调用频率,避免暴力破解
@Post('token')
@ApiOperation({ summary: '邮箱登录' })
async loginByEmail(@Body() user: LoginInputDto) {
return this.usersService.loginByEmail(user.email, user.password)
}
@Patch(':id/?field=password')
@ApiOperation({ summary: '找回密码' })
async forgetPassword(
@Body() payload: ResetPassword,
@Param('id') userId: string,
) {
return this.userService.deleteUser(userData, 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 '修改邮箱'
return this.usersService.resetPasswordByEmail(payload, userId)
}
}

View File

@ -3,12 +3,12 @@ import { UsersService } from './users.service'
import { UsersController } from './users.controller'
import { JwtService } from '@nestjs/jwt'
import { EmailService } from 'src/email/email.service'
import { TokenController } from './token.controller'
import { TokenService } from './token.service'
import { MeService } from './me.service'
import { MeController } from './me.controller'
@Module({
controllers: [UsersController, TokenController],
providers: [UsersService, JwtService, EmailService, TokenService],
controllers: [UsersController, MeController],
providers: [UsersService, JwtService, EmailService, MeService],
exports: [UsersService],
})
export class UsersModule {}

View File

@ -1,15 +1,13 @@
import { Inject, Injectable, ForbiddenException } from '@nestjs/common'
import { Inject, Injectable, 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 { securityConfig, type SecurityConfig } from 'src/common/configs'
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 { TokenService } from './token.service'
import { ForgetPassword } from './dto/forget-password.dto'
import { DeleteUserDto } from './dto/delete-user.dto'
import { ChangePassword } from './dto/change-password.dto'
import { ResetPassword } from './dto/reset-password.dto'
import { Token, TokenPayload } from './dto/token.dto'
@Injectable()
export class UsersService {
@ -17,109 +15,70 @@ export class UsersService {
private jwtService: JwtService,
private prismaService: PrismaService,
private emailService: EmailService,
private tokenService: TokenService,
@Inject(securityConfig.KEY)
private secureConfig: SecurityConfig,
@Inject(securityConfig.KEY) private secureConfig: SecurityConfig,
) {}
async register(userToCreate: CreateUserDto) {
await this.verifyEmail(
userToCreate.email,
userToCreate.token,
userToCreate.verifyCode,
EmailScene.register,
)
async registerByEmail(userToCreate: CreateUserDto) {
const { email, token, verifyCode, username, password } = userToCreate
await this.emailService.verifyEmail({
email,
token,
verifyCode,
scene: EmailScene.register,
})
const hashedPassword = await bcrypt.hash(
userToCreate.password,
password,
this.secureConfig.bcryptSaltOrRound,
)
const user = await this.prismaService.user.create({
data: {
username: userToCreate.username,
email: userToCreate.email,
password: hashedPassword,
},
data: { username, email, password: hashedPassword },
})
return this.tokenService.generateTokens({ userId: user.id })
return this.generateTokens({ userId: user.id })
}
async deleteUser(userToDelete: DeleteUserDto, userId: string) {
await this.verifyEmail(
userToDelete.email,
userToDelete.token,
userToDelete.verifyCode,
EmailScene.deleteUser,
)
async loginByEmail(email: string, password: string) {
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(
user.password,
userToDelete.password,
)
const passwordValid = await bcrypt.compare(password, user.password)
if (!passwordValid) {
throw new ForbiddenException('Invalid password')
throw new UnauthorizedException('Invalid password')
}
return this.prismaService.user.delete({
where: { email: userToDelete.email },
})
return this.generateTokens({ userId: user.id })
}
async changePasswor(payload: ChangePassword, userId: string) {
const user = await this.prismaService.user.findFirstOrThrow({
where: { id: userId },
async resetPasswordByEmail(data: ResetPassword, userId: string) {
const { email, token, verifyCode, password } = data
await this.emailService.verifyEmail({
email,
token,
verifyCode,
scene: EmailScene.forgetPassword,
})
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 forgetPassword(payload: ForgetPassword) {
await this.verifyEmail(
payload.email,
payload.token,
payload.verifyCode,
EmailScene.forgetPassword,
)
const hashedPassword = await bcrypt.hash(
payload.password,
password,
this.secureConfig.bcryptSaltOrRound,
)
const user = await this.prismaService.user.update({
where: { email: payload.email },
where: { id: userId },
data: { password: hashedPassword },
})
return user
return this.generateTokens({ userId: user.id })
}
private async checkPassword(pwd: string, hashPwd: string) {
const valid = await bcrypt.compare(pwd, hashPwd)
if (!valid) {
throw new ForbiddenException('Invalid password')
}
}
private async verifyEmail(
email: string,
token: string,
verifyCode: string,
scene: EmailScene,
) {
const payload = this.jwtService.verify<EmailSendDto>(token, {
secret: this.emailService.getEmailJwtSecret(verifyCode, scene),
private generateTokens(payload: TokenPayload): Token {
const accessToken = this.jwtService.sign(payload, {
secret: this.secureConfig.jwt_access_secret,
expiresIn: this.secureConfig.expiresIn,
})
if (payload.email !== email || payload.scene !== scene) {
throw new ForbiddenException('请输入正确的邮箱验证码')
}
const refreshToken = this.jwtService.sign(payload, {
secret: this.secureConfig.jwt_refresh_secret,
expiresIn: this.secureConfig.refreshIn,
})
return { accessToken, refreshToken, ...payload }
}
}