refactor: changed sendOTP and verifyOTP logic
This commit is contained in:
@@ -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()
|
||||
});
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -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')
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)));
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user