refactor: changed sendOTP and verifyOTP logic

This commit is contained in:
2026-07-29 21:00:39 +03:30
parent ce0b98783c
commit ac4e34436a
6 changed files with 95 additions and 36 deletions
+7 -1
View File
@@ -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()
});
+11 -2
View File
@@ -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;
@ApiPropertyOptional({
example: 'normal_user',
enum: RoleType,
})
@IsOptional()
@IsEnum(RoleType)
role?: RoleType;
}
+11 -2
View File
@@ -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;
}
+1 -1
View File
@@ -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')
+16 -6
View File
@@ -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<void> {
@@ -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) {
+48 -23
View File
@@ -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<T = any> {
code: string;
tries: number;
metadata: T; // holds role, etc...
}
type OtpParams<T extends Record<string, any> = {}> = 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<number>('CACHE_TTL');
}
private getKey(phoneNumber: string) {
private buildKey(phoneNumber: string, metadata: Record<string, any>): 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<string> {
const code = Math.floor(100000 + Math.random() * 900000).toString();
async generate<T extends Record<string, any>>(
params: OtpParams<T>,
): Promise<string> {
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<T> = {
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<boolean> {
const otp = await this.cache.get<OtpCache>(this.getKey(phoneNumber));
async verify<T extends Record<string, any>>(
params: OtpParams<T>,
code: string,
): Promise<boolean> {
const { phoneNumber, ...metadata } = params;
const key = this.buildKey(phoneNumber, metadata);
if (!otp) {
throw new BadRequestException(OTPMessages.OTP_EXPIRED);
}
const otp = await this.cache.get<OtpCache<T>>(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<void> {
await this.cache.del(this.getKey(phoneNumber));
async delete<T extends Record<string, any>>(
params: OtpParams<T>,
): Promise<void> {
const { phoneNumber, ...metadata } = params;
await this.cache.del(this.buildKey(phoneNumber, metadata));
}
async exists(phoneNumber: string): Promise<boolean> {
return !!(await this.cache.get(this.getKey(phoneNumber)));
async exists<T extends Record<string, any>>(
params: OtpParams<T>,
): Promise<boolean> {
const { phoneNumber, ...metadata } = params;
return !!(await this.cache.get(this.buildKey(phoneNumber, metadata)));
}
}