From ac4e34436a9109836322c2819c5e42e2d5565c4c Mon Sep 17 00:00:00 2001 From: realrafi Date: Wed, 29 Jul 2026 21:00:39 +0330 Subject: [PATCH] refactor: changed sendOTP and verifyOTP logic --- src/config/config.validation.ts | 8 ++- src/modules/auth/DTO/send-otp.dto.ts | 15 ++++-- src/modules/auth/DTO/verify-otp.dto.ts | 13 ++++- src/modules/auth/auth.controller.ts | 2 +- src/modules/auth/auth.service.ts | 22 +++++--- src/modules/auth/otp.service.ts | 71 +++++++++++++++++--------- 6 files changed, 95 insertions(+), 36 deletions(-) diff --git a/src/config/config.validation.ts b/src/config/config.validation.ts index f6f1950..eb1c09d 100644 --- a/src/config/config.validation.ts +++ b/src/config/config.validation.ts @@ -21,5 +21,11 @@ export const validationSchema = Joi.object({ JWT_ACCESS_SECRET: Joi.string().required(), JWT_REFRESH_SECRET: Joi.string().required(), JWT_ACCESS_EXPIRES: Joi.string().required(), - JWT_REFRESH_EXPIRES: Joi.string().required() + JWT_REFRESH_EXPIRES: Joi.string().required(), + + // CACHE Config + CACHE_TTL : Joi.number().required(), + + // Redis Config + REDIS_URL : Joi.string().required() }); \ No newline at end of file diff --git a/src/modules/auth/DTO/send-otp.dto.ts b/src/modules/auth/DTO/send-otp.dto.ts index 4d3e945..8a63b46 100644 --- a/src/modules/auth/DTO/send-otp.dto.ts +++ b/src/modules/auth/DTO/send-otp.dto.ts @@ -1,5 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { Matches } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsEnum, IsOptional, Matches } from 'class-validator'; +import { RoleType } from 'src/modules/users/enums/user-role.enum'; export class SendOtpDto { @ApiProperty({ @@ -9,4 +10,12 @@ export class SendOtpDto { message: 'Phone number must be a valid Iranian mobile number.', }) phoneNumber!: string; -} \ No newline at end of file + + @ApiPropertyOptional({ + example: 'normal_user', + enum: RoleType, + }) + @IsOptional() + @IsEnum(RoleType) + role?: RoleType; +} diff --git a/src/modules/auth/DTO/verify-otp.dto.ts b/src/modules/auth/DTO/verify-otp.dto.ts index 838f60c..1a9cad9 100644 --- a/src/modules/auth/DTO/verify-otp.dto.ts +++ b/src/modules/auth/DTO/verify-otp.dto.ts @@ -1,5 +1,6 @@ -import { ApiProperty } from '@nestjs/swagger'; -import { IsString, Matches, Length } from 'class-validator'; +import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger'; +import { IsString, Matches, Length, IsEnum, IsOptional } from 'class-validator'; +import { RoleType } from 'src/modules/users/enums/user-role.enum'; export class VerifyOtpDto { @ApiProperty({ @@ -14,4 +15,12 @@ export class VerifyOtpDto { @IsString() @Length(6, 6) code: string; + + @ApiPropertyOptional({ + example: 'normal_user', + enum: RoleType, + }) + @IsOptional() + @IsEnum(RoleType) + role?: RoleType; } \ No newline at end of file diff --git a/src/modules/auth/auth.controller.ts b/src/modules/auth/auth.controller.ts index bdb155b..9f7599e 100644 --- a/src/modules/auth/auth.controller.ts +++ b/src/modules/auth/auth.controller.ts @@ -20,7 +20,7 @@ export class AuthController { @ApiResponse({ status: 200, description: 'Sent Otp Successfully!' }) @ApiResponse({ status: 400, description: 'Bad Request from User!' }) async sendOtp(@Body() dto: SendOtpDto) { - return this.authService.sendOTP(dto.phoneNumber); + return this.authService.sendOTP(dto); } @Post('verify-otp') diff --git a/src/modules/auth/auth.service.ts b/src/modules/auth/auth.service.ts index a043884..9810e61 100644 --- a/src/modules/auth/auth.service.ts +++ b/src/modules/auth/auth.service.ts @@ -13,6 +13,9 @@ import * as bcrypt from 'bcrypt'; import { JwtService } from '@nestjs/jwt'; import ms from 'ms'; import { OtpService } from './otp.service'; +import { RoleType } from '../users/enums/user-role.enum'; +import { SendOtpDto } from './DTO/send-otp.dto'; +import { string } from 'joi'; @Injectable() export class AuthService { @@ -22,15 +25,18 @@ export class AuthService { private readonly configService: ConfigService, private readonly userService: UsersService, private readonly jwtService: JwtService, - private readonly otpService: OtpService + private readonly otpService: OtpService, ) {} - async sendOTP(phone: string) { - const otp = await this.otpService.generate(phone); + async sendOTP(dto: SendOtpDto) { + const otp = await this.otpService.generate({ + phoneNumber: dto.phoneNumber, + role: dto.role, + }); try { await this.smsService.sendSms({ - Mobile: phone, + Mobile: dto.phoneNumber, TemplateId: this.smsConfig.SMS_PATTERN_OTP, Parameters: [ { @@ -51,7 +57,11 @@ export class AuthService { } async verifyOTP(dto: VerifyOtpDto) { - await this.otpService.verify(dto.phoneNumber, dto.code); + await this.otpService.verify( + { phoneNumber: dto.phoneNumber, role: dto.role }, + dto.code, + ); + if (dto.role) console.log('VERIFIED OTP FOR ROLE: ', dto.role); } async checkLoginCredentials(credential: LoginCredentialDto): Promise { @@ -65,7 +75,7 @@ export class AuthService { throw new BadRequestException(UserMessages.USER_PASSWORD_INVALID); } - await this.sendOTP(phoneNumber); + await this.sendOTP({ phoneNumber } as SendOtpDto); } async verifyOtpAndLogin(dto: VerifyOtpDto) { diff --git a/src/modules/auth/otp.service.ts b/src/modules/auth/otp.service.ts index e4bc8cd..6e52115 100644 --- a/src/modules/auth/otp.service.ts +++ b/src/modules/auth/otp.service.ts @@ -2,17 +2,25 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager'; import { BadRequestException, Inject, Injectable } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; import type { Cache } from 'cache-manager'; +import { metadata } from 'reflect-metadata/no-conflict'; import { OTPMessages } from 'src/common/enums/messages.enum'; -interface OtpCache { +interface OtpCache { code: string; tries: number; + metadata: T; // holds role, etc... } +type OtpParams = {}> = T & { + phoneNumber: string; +}; + @Injectable() export class OtpService { private readonly TTL: number; + private readonly keyTemplate: (keyof any)[] = ['role']; // could add more later if necessary + constructor( @Inject(CACHE_MANAGER) private readonly cache: Cache, @@ -21,54 +29,71 @@ export class OtpService { this.TTL = this.configService.getOrThrow('CACHE_TTL'); } - private getKey(phoneNumber: string) { + private buildKey(phoneNumber: string, metadata: Record): string { + const prefixParts = this.keyTemplate + .map((field) => metadata[field as string]) + .filter((value) => value !== undefined && value !== null); + + if (prefixParts.length > 0) { + return `otp:${prefixParts.join(':')}:${phoneNumber}`; + } return `otp:${phoneNumber}`; } - async generate(phoneNumber: string): Promise { - const code = Math.floor(100000 + Math.random() * 900000).toString(); + async generate>( + params: OtpParams, + ): Promise { + const { phoneNumber, ...metadata } = params; + const key = this.buildKey(phoneNumber, metadata); + const code = Math.floor(100_000 + Math.random() * 900_000).toString(); - const value: OtpCache = { + const value: OtpCache = { code, tries: 0, + metadata: metadata as unknown as T, }; - await this.cache.set(this.getKey(phoneNumber), value, this.TTL); - + await this.cache.set(key, value, this.TTL); return code; } - async verify(phoneNumber: string, code: string): Promise { - const otp = await this.cache.get(this.getKey(phoneNumber)); + async verify>( + params: OtpParams, + code: string, + ): Promise { + const { phoneNumber, ...metadata } = params; + const key = this.buildKey(phoneNumber, metadata); - if (!otp) { - throw new BadRequestException(OTPMessages.OTP_EXPIRED); - } + const otp = await this.cache.get>(key); + + if (!otp) throw new BadRequestException(OTPMessages.OTP_EXPIRED); if (otp.tries >= 5) { - await this.cache.del(this.getKey(phoneNumber)); - + await this.cache.del(key); throw new BadRequestException(OTPMessages.OTP_TOO_MANY_TRIES); } if (otp.code !== code) { otp.tries++; - - await this.cache.set(this.getKey(phoneNumber), otp, this.TTL); - + await this.cache.set(key, otp, this.TTL); throw new BadRequestException(OTPMessages.OTP_INVALID); } - await this.cache.del(this.getKey(phoneNumber)); - + await this.cache.del(key); return true; } - async delete(phoneNumber: string): Promise { - await this.cache.del(this.getKey(phoneNumber)); + async delete>( + params: OtpParams, + ): Promise { + const { phoneNumber, ...metadata } = params; + await this.cache.del(this.buildKey(phoneNumber, metadata)); } - async exists(phoneNumber: string): Promise { - return !!(await this.cache.get(this.getKey(phoneNumber))); + async exists>( + params: OtpParams, + ): Promise { + const { phoneNumber, ...metadata } = params; + return !!(await this.cache.get(this.buildKey(phoneNumber, metadata))); } }