diff --git a/src/email/email.service.ts b/src/email/email.service.ts
index 775bb6d..2265611 100644
--- a/src/email/email.service.ts
+++ b/src/email/email.service.ts
@@ -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]},验证码为 ${verifyCode},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(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
}
diff --git a/src/users/dto/forget-password.dto.ts b/src/users/dto/reset-password.dto.ts
similarity index 89%
rename from src/users/dto/forget-password.dto.ts
rename to src/users/dto/reset-password.dto.ts
index f766e99..0b6ac38 100644
--- a/src/users/dto/forget-password.dto.ts
+++ b/src/users/dto/reset-password.dto.ts
@@ -1,6 +1,6 @@
import { IsEmail, IsNotEmpty, IsStrongPassword } from 'class-validator'
-export class ForgetPassword {
+export class ResetPassword {
@IsNotEmpty()
@IsEmail()
email: string
diff --git a/src/users/me.controller.ts b/src/users/me.controller.ts
new file mode 100644
index 0000000..a7f211e
--- /dev/null
+++ b/src/users/me.controller.ts
@@ -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 {
+ return this.prismaService.user.findUniqueOrThrow({
+ where: { id: userId },
+ })
+ }
+
+ @Patch()
+ @ApiOperation({ summary: '修改用户信息(用户名等)' })
+ @UseInterceptors(PasswordInterceptor)
+ @NeedAuth()
+ async updateUserInfo(
+ @Body() payload: UpdateUserDto,
+ @User('userId') userId: string,
+ ): Promise {
+ 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)
+ }
+}
diff --git a/src/users/me.service.ts b/src/users/me.service.ts
new file mode 100644
index 0000000..d138b06
--- /dev/null
+++ b/src/users/me.service.ts
@@ -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(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')
+ }
+ }
+}
diff --git a/src/users/token.controller.ts b/src/users/token.controller.ts
deleted file mode 100644
index 8b3e49c..0000000
--- a/src/users/token.controller.ts
+++ /dev/null
@@ -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)
- }
-}
diff --git a/src/users/token.service.ts b/src/users/token.service.ts
deleted file mode 100644
index c863d29..0000000
--- a/src/users/token.service.ts
+++ /dev/null
@@ -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(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 }
- }
-}
diff --git a/src/users/users.controller.ts b/src/users/users.controller.ts
index 9840f30..8e4548b 100644
--- a/src/users/users.controller.ts
+++ b/src/users/users.controller.ts
@@ -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 {
- 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 {
- 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 {
- 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)
}
}
diff --git a/src/users/users.module.ts b/src/users/users.module.ts
index 3f60d8c..68a673c 100644
--- a/src/users/users.module.ts
+++ b/src/users/users.module.ts
@@ -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 {}
diff --git a/src/users/users.service.ts b/src/users/users.service.ts
index 9566fed..99642b8 100644
--- a/src/users/users.service.ts
+++ b/src/users/users.service.ts
@@ -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(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 }
}
}