diff --git a/src/app.module.ts b/src/app.module.ts index 556ea37..a316af8 100644 --- a/src/app.module.ts +++ b/src/app.module.ts @@ -2,8 +2,8 @@ import { Logger, Module } from '@nestjs/common' import { ConfigModule } from '@nestjs/config' import { nestConfig, securityConfig, emailConfig } from 'src/common/configs' import { PrismaModule, loggingMiddleware } from 'nestjs-prisma' +import { JwtAuthStrategy } from './common/guards/jwt-auth.strategy' import { UsersModule } from './users/users.module' -import { AuthModule } from './auth/auth.module' import { EmailModule } from './email/email.module' @Module({ @@ -25,9 +25,8 @@ import { EmailModule } from './email/email.module' }), UsersModule, - AuthModule, EmailModule, ], - controllers: [], + providers: [JwtAuthStrategy], }) export class AppModule {} diff --git a/src/auth/auth.controller.ts b/src/auth/auth.controller.ts deleted file mode 100644 index ff82041..0000000 --- a/src/auth/auth.controller.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { Body, Controller, Post, Put } from '@nestjs/common' -import { AuthService } from './auth.service' -import { CreateUserDto } from 'src/users/dto/create-user.dto' -import { ApiTags, ApiOperation, ApiUnauthorizedResponse } from '@nestjs/swagger' -import { LoginInputDto } from './dto/login-input.dto' -import { TokenRefreshPayload } from './dto/token.dto' - -@ApiTags('Auth') -@Controller('api/auth') -export class AuthController { - constructor(private readonly authService: AuthService) {} - - @ApiOperation({ summary: '注册用户,返回token' }) - @Post('register') - async register(@Body() userData: CreateUserDto) { - return this.authService.register(userData) - } - - @ApiOperation({ summary: '登录用户,返回token' }) - @Post('login') - async login(@Body() user: LoginInputDto) { - return this.authService.login(user.email, user.password) - } - - @ApiOperation({ summary: '刷新token' }) - @ApiUnauthorizedResponse({ description: 'Unauthorized' }) - @Put('token') - async refreshToken(@Body() payload: TokenRefreshPayload) { - return this.authService.refreshToken(payload.refreshToken) - } -} diff --git a/src/auth/auth.module.ts b/src/auth/auth.module.ts deleted file mode 100644 index 6a2c213..0000000 --- a/src/auth/auth.module.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { Module } from '@nestjs/common' -import { AuthService } from './auth.service' -import { AuthController } from './auth.controller' -import { JwtService } from '@nestjs/jwt' -import { JwtStrategy } from './strategies/jwt.strategy' -import { EmailService } from 'src/email/email.service' - -@Module({ - controllers: [AuthController], - providers: [AuthService, JwtService, JwtStrategy, EmailService], -}) -export class AuthModule {} diff --git a/src/auth/auth.service.ts b/src/auth/auth.service.ts deleted file mode 100644 index 248d72b..0000000 --- a/src/auth/auth.service.ts +++ /dev/null @@ -1,95 +0,0 @@ -import { - Inject, - Injectable, - ForbiddenException, - UnauthorizedException, -} from '@nestjs/common' -import * as bcrypt from 'bcrypt' -import { PrismaService } from 'nestjs-prisma' -import { Token, TokenPayload } from './dto/token.dto' -import { JwtService } from '@nestjs/jwt' -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' - -@Injectable() -export class AuthService { - constructor( - private jwtService: JwtService, - private prismaService: PrismaService, - private emailService: EmailService, - @Inject(securityConfig.KEY) - private secureConfig: SecurityConfig, - ) {} - - async register(userToCreate: CreateUserDto) { - const tokenPayload = this.jwtService.verify( - userToCreate.token, - { - secret: this.emailService.getEmailJwtSecret( - userToCreate.verificationCode, - EmailScene.register, - ), - }, - ) - if ( - tokenPayload.email !== userToCreate.email || - tokenPayload.scene !== 'register' - ) { - throw new ForbiddenException('请输入正确的邮箱') - } - - const hashedPassword = await bcrypt.hash( - userToCreate.password, - this.secureConfig.bcryptSaltOrRound, - ) - const user = await this.prismaService.user.create({ - data: { - username: userToCreate.username, - email: userToCreate.email, - password: hashedPassword, - }, - }) - return this.generateTokens({ userId: user.id }) - } - - 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 ForbiddenException('Invalid password') - } - - return this.generateTokens({ userId: user.id }) - } - - async refreshToken(token: string) { - try { - const { userId } = this.jwtService.verify(token, { - secret: this.secureConfig.jwt_refresh_secret, - }) - return this.generateTokens({ userId }) - } catch (e) { - console.error(e) - throw new UnauthorizedException(e.message) - } - } - - private 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/common/decorators/user.decorator.ts b/src/common/decorators/user.decorator.ts index 985d5f2..f2b41db 100644 --- a/src/common/decorators/user.decorator.ts +++ b/src/common/decorators/user.decorator.ts @@ -1,6 +1,6 @@ import { createParamDecorator, ExecutionContext } from '@nestjs/common' import { type Request } from 'express' -import { TokenPayload } from 'src/auth/dto/token.dto' +import { TokenPayload } from 'src/users/dto/token.dto' export const User = createParamDecorator( (key: keyof TokenPayload, ctx: ExecutionContext) => { diff --git a/src/common/filters/jwt-exceptions.filter.ts b/src/common/filters/jwt-exceptions.filter.ts index 3df38ff..3ea1902 100644 --- a/src/common/filters/jwt-exceptions.filter.ts +++ b/src/common/filters/jwt-exceptions.filter.ts @@ -2,7 +2,7 @@ import { Catch, Logger, ArgumentsHost, - ForbiddenException, + UnauthorizedException, } from '@nestjs/common' import { BaseExceptionFilter } from '@nestjs/core' import { JsonWebTokenError } from 'jsonwebtoken' @@ -13,6 +13,6 @@ export class JwtExceptionsFilter extends BaseExceptionFilter { catch(exception: JsonWebTokenError, host: ArgumentsHost) { this.logger.error(exception) - super.catch(new ForbiddenException(exception.message), host) + super.catch(new UnauthorizedException(exception.message), host) } } diff --git a/src/auth/strategies/jwt.strategy.ts b/src/common/guards/jwt-auth.strategy.ts similarity index 81% rename from src/auth/strategies/jwt.strategy.ts rename to src/common/guards/jwt-auth.strategy.ts index c258ee6..3155d82 100644 --- a/src/auth/strategies/jwt.strategy.ts +++ b/src/common/guards/jwt-auth.strategy.ts @@ -1,11 +1,11 @@ import { Strategy, ExtractJwt } from 'passport-jwt' import { PassportStrategy } from '@nestjs/passport' import { Injectable, Inject } from '@nestjs/common' -import { TokenPayload } from '../dto/token.dto' +import { TokenPayload } from 'src/users/dto/token.dto' import { securityConfig, SecurityConfig } from 'src/common/configs' @Injectable() -export class JwtStrategy extends PassportStrategy(Strategy) { +export class JwtAuthStrategy extends PassportStrategy(Strategy) { constructor( @Inject(securityConfig.KEY) readonly secureConfig: SecurityConfig, diff --git a/src/email/email.controller.ts b/src/email/email.controller.ts index cad8333..dc5db09 100644 --- a/src/email/email.controller.ts +++ b/src/email/email.controller.ts @@ -15,7 +15,7 @@ export class EmailController { // } @ApiOperation({ summary: '发送邮箱验证码' }) - @Post() + @Post('verificationCode') async getRegisterToken(@Body() payload: EmailSendDto) { return this.emailService.getRegisterToken(payload.email, payload.scene) } diff --git a/src/email/email.service.ts b/src/email/email.service.ts index 458d20b..dd15eb4 100644 --- a/src/email/email.service.ts +++ b/src/email/email.service.ts @@ -33,7 +33,7 @@ export class EmailService { if (user) { throw new ConflictException(`邮箱${email}已注册`) } - const verificationCode = this.generateRandomNum() + const verificationCode = this.generateVerificationCode() const registerToken = this.jwtService.sign( { email, scene }, { secret: this.getEmailJwtSecret(verificationCode, scene) }, @@ -50,7 +50,7 @@ export class EmailService { return this.secureConfig.jwt_access_secret + verificationCode + scene } - private generateRandomNum() { + private generateVerificationCode() { return Math.floor(Math.random() * 899999 + 100000).toString() } } diff --git a/src/auth/dto/login-input.dto.ts b/src/users/dto/login-input.dto.ts similarity index 100% rename from src/auth/dto/login-input.dto.ts rename to src/users/dto/login-input.dto.ts diff --git a/src/auth/dto/token.dto.ts b/src/users/dto/token.dto.ts similarity index 100% rename from src/auth/dto/token.dto.ts rename to src/users/dto/token.dto.ts diff --git a/src/users/token.controller.ts b/src/users/token.controller.ts new file mode 100644 index 0000000..17ddd3a --- /dev/null +++ b/src/users/token.controller.ts @@ -0,0 +1,24 @@ +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) {} + + @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 new file mode 100644 index 0000000..b9f4064 --- /dev/null +++ b/src/users/token.service.ts @@ -0,0 +1,50 @@ +import { Inject, Injectable, ForbiddenException } from '@nestjs/common' +import * as bcrypt from 'bcrypt' +import { PrismaService } from 'nestjs-prisma' +import { Token, TokenPayload } 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 ForbiddenException('Invalid password') + } + + return this.generateTokens({ userId: user.id }) + } + + async refreshToken(token: string) { + const { userId } = this.jwtService.verify(token, { + secret: this.secureConfig.jwt_refresh_secret, + }) + return this.generateTokens({ userId }) + } + + 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 1c45339..8c13848 100644 --- a/src/users/users.controller.ts +++ b/src/users/users.controller.ts @@ -1,11 +1,12 @@ -import { Controller, Get, UseInterceptors } from '@nestjs/common' +import { Controller, Get, Post, Body, UseInterceptors } from '@nestjs/common' import { UsersService } from './users.service' -import { ApiOperation, ApiTags } 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' @ApiTags('User') @Controller('api/users') @@ -22,4 +23,10 @@ export class UsersController { async getUserInfo(@User('userId') userId: string): Promise { return this.prismaService.user.findUniqueOrThrow({ where: { id: userId } }) } + + @ApiOperation({ summary: '注册用户' }) + @Post() + async register(@Body() userData: CreateUserDto) { + return this.userService.register(userData) + } } diff --git a/src/users/users.module.ts b/src/users/users.module.ts index 2ca0e52..3f60d8c 100644 --- a/src/users/users.module.ts +++ b/src/users/users.module.ts @@ -1,10 +1,14 @@ import { Module } from '@nestjs/common' 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' @Module({ - controllers: [UsersController], - providers: [UsersService], + controllers: [UsersController, TokenController], + providers: [UsersService, JwtService, EmailService, TokenService], exports: [UsersService], }) export class UsersModule {} diff --git a/src/users/users.service.ts b/src/users/users.service.ts index c35c3d3..1d41738 100644 --- a/src/users/users.service.ts +++ b/src/users/users.service.ts @@ -1,7 +1,61 @@ -import { Injectable } from '@nestjs/common' +import { Inject, Injectable, ForbiddenException } 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 { 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' @Injectable() export class UsersService { - constructor(private prismaService: PrismaService) {} + constructor( + private jwtService: JwtService, + private prismaService: PrismaService, + private emailService: EmailService, + private tokenService: TokenService, + @Inject(securityConfig.KEY) + private secureConfig: SecurityConfig, + ) {} + + async register(userToCreate: CreateUserDto) { + await this.verifyEmail( + userToCreate.email, + userToCreate.token, + userToCreate.verificationCode, + EmailScene.register, + ) + return this.createUser(userToCreate) + } + + private async verifyEmail( + email: string, + token: string, + verificationCode: string, + scene: EmailScene, + ) { + const payload = this.jwtService.verify(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, + ) + const user = await this.prismaService.user.create({ + data: { + username: userToCreate.username, + email: userToCreate.email, + password: hashedPassword, + }, + }) + return this.tokenService.generateTokens({ userId: user.id }) + } }