Skip to content

Commit

Permalink
otp login
Browse files Browse the repository at this point in the history
  • Loading branch information
nghiavohuynhdai committed Jun 24, 2024
1 parent f255110 commit 0830dd5
Show file tree
Hide file tree
Showing 8 changed files with 207 additions and 6 deletions.
8 changes: 6 additions & 2 deletions src/auth/auth.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ import { ConfigModule } from '@nestjs/config'
import { JwtRefreshStrategy } from '@auth/strategies/jwt-refresh.strategy'
import { StaffModule } from '@staff/staff.module'
import { AuthProviderController } from '@auth/controllers/provider.controller'
import { OtpRepository } from './repositories/otp.repository'
import { MongooseModule } from '@nestjs/mongoose'
import { Otp, OtpSchema } from './schema/otp.schema'

@Global()
@Module({
Expand All @@ -17,10 +20,11 @@ import { AuthProviderController } from '@auth/controllers/provider.controller'
CustomerModule,
StaffModule,
PassportModule,
JwtModule
JwtModule,
MongooseModule.forFeature([{ name: Otp.name, schema: OtpSchema }])
],
controllers: [AuthCustomerController, AuthProviderController],
providers: [AuthService, JwtAccessStrategy, JwtRefreshStrategy],
providers: [AuthService, JwtAccessStrategy, JwtRefreshStrategy, OtpRepository],
exports: [AuthService]
})
export class AuthModule {}
20 changes: 18 additions & 2 deletions src/auth/controllers/customer.controller.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import { Body, Controller, Post, Req, UseGuards } from '@nestjs/common'
import { ApiBadRequestResponse, ApiBearerAuth, ApiBody, ApiOkResponse, ApiTags } from '@nestjs/swagger'
import { ErrorResponse, SuccessDataResponse } from '@common/contracts/dto'
import { GoogleLoginReqDto, LoginReqDto } from '@auth/dto/login.dto'
import { ErrorResponse, SuccessDataResponse, SuccessResponse } from '@common/contracts/dto'
import { GoogleLoginReqDto, LoginReqDto, VerifyOtpReqDto } from '@auth/dto/login.dto'
import { AuthService } from '@auth/services/auth.service'
import { TokenResDto } from '@auth/dto/token.dto'
import { UserSide } from '@common/contracts/constant'
Expand Down Expand Up @@ -29,6 +29,22 @@ export class AuthCustomerController {
return this.authService.googleLogin(googleLoginReqDto)
}

@Post('login-otp')
@ApiBody({ type: LoginReqDto })
@ApiOkResponse({ type: DataResponse(SuccessResponse) })
@ApiBadRequestResponse({ type: ErrorResponse })
loginOtp(@Body() loginReqDto: LoginReqDto): Promise<SuccessResponse> {
return this.authService.loginOtp(loginReqDto, UserSide.CUSTOMER)
}

@Post('verify-otp')
@ApiBody({ type: VerifyOtpReqDto })
@ApiOkResponse({ type: DataResponse(TokenResDto) })
@ApiBadRequestResponse({ type: ErrorResponse })
verifyOtp(@Body() verifyOtpReqDto: VerifyOtpReqDto): Promise<TokenResDto> {
return this.authService.verifyOtp(verifyOtpReqDto, UserSide.CUSTOMER)
}

@Post('register')
@ApiBody({ type: RegisterReqDto })
@ApiOkResponse({ type: SuccessDataResponse })
Expand Down
10 changes: 10 additions & 0 deletions src/auth/dto/login.dto.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,16 @@ export class LoginReqDto {
password: string;
}

export class VerifyOtpReqDto {
@ApiProperty()
@IsNotEmpty()
email: string;

@ApiProperty()
@IsNotEmpty()
otp: string;
}

export class GoogleLoginReqDto {
@ApiProperty()
@IsNotEmpty()
Expand Down
13 changes: 13 additions & 0 deletions src/auth/repositories/otp.repository.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import { PaginateModel } from 'mongoose'
import { Injectable } from '@nestjs/common'
import { InjectModel } from '@nestjs/mongoose'

import { AbstractRepository } from '@common/repositories'
import { Otp, OtpDocument } from '@auth/schema/otp.schema'

@Injectable()
export class OtpRepository extends AbstractRepository<OtpDocument> {
constructor(@InjectModel(Otp.name) model: PaginateModel<OtpDocument>) {
super(model)
}
}
39 changes: 39 additions & 0 deletions src/auth/schema/otp.schema.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
import { Prop, Schema, SchemaFactory } from '@nestjs/mongoose'
import { HydratedDocument } from 'mongoose'
import * as paginate from 'mongoose-paginate-v2'
import { Transform } from 'class-transformer'

export type OtpDocument = HydratedDocument<Otp>

@Schema({
collection: 'otps',
timestamps: {
createdAt: true,
updatedAt: true
},
toJSON: {
transform(doc, ret) {
delete ret.__v
}
}
})
export class Otp {
constructor(id?: string) {
this._id = id
}
@Transform(({ value }) => value?.toString())
_id: string

@Prop({ type: String, required: true })
otp: string

@Prop({ type: String, required: true, index: true })
customerId: string

@Prop({ type: Date, required: true })
expiredAt: Date
}

export const OtpSchema = SchemaFactory.createForClass(Otp)

OtpSchema.plugin(paginate)
110 changes: 108 additions & 2 deletions src/auth/services/auth.service.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import { OtpRepository } from './../repositories/otp.repository'
import { JwtService } from '@nestjs/jwt'
import { BadRequestException, Injectable, UnauthorizedException } from '@nestjs/common'
import { GoogleLoginReqDto, LoginReqDto } from '@auth/dto/login.dto'
import { GoogleLoginReqDto, LoginReqDto, VerifyOtpReqDto } from '@auth/dto/login.dto'
import { CustomerRepository } from '@customer/repositories/customer.repository'
import { Errors } from '@common/contracts/error'
import { Customer } from '@customer/schemas/customer.schema'
Expand All @@ -15,14 +16,17 @@ import { StaffRepository } from '@staff/repositories/staff.repository'
import { Staff } from '@staff/schemas/staff.schema'
import { SuccessResponse } from '@common/contracts/dto'
import { OAuth2Client } from 'google-auth-library'
import { MailerService } from '@nestjs-modules/mailer'

@Injectable()
export class AuthService {
constructor(
private readonly customerRepository: CustomerRepository,
private readonly staffRepository: StaffRepository,
private readonly otpRepository: OtpRepository,
private readonly jwtService: JwtService,
private readonly configService: ConfigService
private readonly configService: ConfigService,
private readonly mailerService: MailerService
) {}

public async login(loginReqDto: LoginReqDto, side: UserSide): Promise<TokenResDto> {
Expand Down Expand Up @@ -71,6 +75,108 @@ export class AuthService {
}
}

public async loginOtp(loginReqDto: LoginReqDto, side: UserSide): Promise<SuccessResponse> {
let user: Customer | Staff
let userRole: UserRole
let providerId: string

if (side === UserSide.CUSTOMER) {
user = await this.customerRepository.findOne({
conditions: {
email: loginReqDto.email
}
})

userRole = UserRole.CUSTOMER
}

if (side === UserSide.PROVIDER) {
user = await this.staffRepository.findOne({
conditions: {
email: loginReqDto.email
}
})

userRole = user?.role
providerId = user?.providerId.toString()
}

if (!user) throw new BadRequestException(Errors.WRONG_EMAIL_OR_PASSWORD.message)

if (user.status === Status.INACTIVE) throw new BadRequestException(Errors.INACTIVE_ACCOUNT.message)

const isPasswordMatch = await this.comparePassword(loginReqDto.password, user.password)

if (!isPasswordMatch) throw new BadRequestException(Errors.WRONG_EMAIL_OR_PASSWORD.message)

const otp = Math.floor(1000 + Math.random() * 9000).toString()
this.otpRepository.create({ otp, customerId: user._id, expiredAt: new Date(Date.now() + 5 * 60000) })

// Send email contain OTP to customer
await this.mailerService.sendMail({
to: loginReqDto.email,
subject: `[Furnique] Mã OTP đăng nhập`,
template: 'login-otp',
context: {
otp
}
})

return new SuccessResponse(true)
}

public async verifyOtp(verifyOtpReqDto: VerifyOtpReqDto, side: UserSide): Promise<TokenResDto> {
let user: Customer | Staff
let userRole: UserRole
let providerId: string

if (side === UserSide.CUSTOMER) {
user = await this.customerRepository.findOne({
conditions: {
email: verifyOtpReqDto.email
}
})

userRole = UserRole.CUSTOMER
}

if (side === UserSide.PROVIDER) {
user = await this.staffRepository.findOne({
conditions: {
email: verifyOtpReqDto.email
}
})

userRole = user?.role
providerId = user?.providerId.toString()
}

if (!user) throw new BadRequestException(Errors.WRONG_EMAIL_OR_PASSWORD.message)

if (user.status === Status.INACTIVE) throw new BadRequestException(Errors.INACTIVE_ACCOUNT.message)

const otp = await this.otpRepository.findOne({
conditions: {
otp: verifyOtpReqDto.otp,
customerId: user._id,
expiredAt: { $gt: new Date() }
}
})

if (!otp) throw new BadRequestException(Errors.WRONG_OTP.message)

const accessTokenPayload: AccessTokenPayload = { name: user.firstName, sub: user._id, role: userRole, providerId }

const refreshTokenPayload: RefreshTokenPayload = { sub: user._id, role: userRole }

const tokens = this.generateTokens(accessTokenPayload, refreshTokenPayload)

return {
accessToken: tokens.accessToken,
refreshToken: tokens.refreshToken
}
}

public async googleLogin(googleLoginReqDto: GoogleLoginReqDto): Promise<TokenResDto> {
const client = new OAuth2Client({
clientId: this.configService.get('GOOGLE_CLIENT_ID'),
Expand Down
5 changes: 5 additions & 0 deletions src/common/contracts/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,11 @@ export const Errors: Record<string, ErrorResponse> = {
message: 'Tài khoản của bạn đã bị vô hiệu hóa. Vui lòng liên lạc với admin.',
httpStatus: HttpStatus.BAD_REQUEST
},
WRONG_OTP: {
error: 'WRONG_OTP',
message: 'Mã OTP không đúng',
httpStatus: HttpStatus.BAD_REQUEST
},
EMAIL_ALREADY_EXIST: {
error: 'EMAIL_ALREADY_EXIST',
message: 'Email đã được sử dụng',
Expand Down
8 changes: 8 additions & 0 deletions src/templates/login-otp.ejs
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
<pre style="font-family: Arial, Helvetica, sans-serif; font-size: 1.5em">
<h2>Email OTP</h2>
<p>Xin chào,</p>
<p>Mã OTP của bạn là: <%= otp %></p>
<p>Vui lòng sử dụng mã OTP này để xác minh tài khoản của bạn.</p>
<p>Cảm ơn bạn!</p>
</pre>
<%- include('web-footer') -%>

0 comments on commit 0830dd5

Please sign in to comment.