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_ACCESS_SECRET: Joi.string().required(),
|
||||||
JWT_REFRESH_SECRET: Joi.string().required(),
|
JWT_REFRESH_SECRET: Joi.string().required(),
|
||||||
JWT_ACCESS_EXPIRES: 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 { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { Matches } from 'class-validator';
|
import { IsEnum, IsOptional, Matches } from 'class-validator';
|
||||||
|
import { RoleType } from 'src/modules/users/enums/user-role.enum';
|
||||||
|
|
||||||
export class SendOtpDto {
|
export class SendOtpDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@@ -9,4 +10,12 @@ export class SendOtpDto {
|
|||||||
message: 'Phone number must be a valid Iranian mobile number.',
|
message: 'Phone number must be a valid Iranian mobile number.',
|
||||||
})
|
})
|
||||||
phoneNumber!: string;
|
phoneNumber!: string;
|
||||||
|
|
||||||
|
@ApiPropertyOptional({
|
||||||
|
example: 'normal_user',
|
||||||
|
enum: RoleType,
|
||||||
|
})
|
||||||
|
@IsOptional()
|
||||||
|
@IsEnum(RoleType)
|
||||||
|
role?: RoleType;
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty, ApiPropertyOptional } from '@nestjs/swagger';
|
||||||
import { IsString, Matches, Length } from 'class-validator';
|
import { IsString, Matches, Length, IsEnum, IsOptional } from 'class-validator';
|
||||||
|
import { RoleType } from 'src/modules/users/enums/user-role.enum';
|
||||||
|
|
||||||
export class VerifyOtpDto {
|
export class VerifyOtpDto {
|
||||||
@ApiProperty({
|
@ApiProperty({
|
||||||
@@ -14,4 +15,12 @@ export class VerifyOtpDto {
|
|||||||
@IsString()
|
@IsString()
|
||||||
@Length(6, 6)
|
@Length(6, 6)
|
||||||
code: string;
|
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: 200, description: 'Sent Otp Successfully!' })
|
||||||
@ApiResponse({ status: 400, description: 'Bad Request from User!' })
|
@ApiResponse({ status: 400, description: 'Bad Request from User!' })
|
||||||
async sendOtp(@Body() dto: SendOtpDto) {
|
async sendOtp(@Body() dto: SendOtpDto) {
|
||||||
return this.authService.sendOTP(dto.phoneNumber);
|
return this.authService.sendOTP(dto);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Post('verify-otp')
|
@Post('verify-otp')
|
||||||
|
|||||||
@@ -13,6 +13,9 @@ import * as bcrypt from 'bcrypt';
|
|||||||
import { JwtService } from '@nestjs/jwt';
|
import { JwtService } from '@nestjs/jwt';
|
||||||
import ms from 'ms';
|
import ms from 'ms';
|
||||||
import { OtpService } from './otp.service';
|
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()
|
@Injectable()
|
||||||
export class AuthService {
|
export class AuthService {
|
||||||
@@ -22,15 +25,18 @@ export class AuthService {
|
|||||||
private readonly configService: ConfigService,
|
private readonly configService: ConfigService,
|
||||||
private readonly userService: UsersService,
|
private readonly userService: UsersService,
|
||||||
private readonly jwtService: JwtService,
|
private readonly jwtService: JwtService,
|
||||||
private readonly otpService: OtpService
|
private readonly otpService: OtpService,
|
||||||
) {}
|
) {}
|
||||||
|
|
||||||
async sendOTP(phone: string) {
|
async sendOTP(dto: SendOtpDto) {
|
||||||
const otp = await this.otpService.generate(phone);
|
const otp = await this.otpService.generate({
|
||||||
|
phoneNumber: dto.phoneNumber,
|
||||||
|
role: dto.role,
|
||||||
|
});
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.smsService.sendSms({
|
await this.smsService.sendSms({
|
||||||
Mobile: phone,
|
Mobile: dto.phoneNumber,
|
||||||
TemplateId: this.smsConfig.SMS_PATTERN_OTP,
|
TemplateId: this.smsConfig.SMS_PATTERN_OTP,
|
||||||
Parameters: [
|
Parameters: [
|
||||||
{
|
{
|
||||||
@@ -51,7 +57,11 @@ export class AuthService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async verifyOTP(dto: VerifyOtpDto) {
|
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> {
|
async checkLoginCredentials(credential: LoginCredentialDto): Promise<void> {
|
||||||
@@ -65,7 +75,7 @@ export class AuthService {
|
|||||||
throw new BadRequestException(UserMessages.USER_PASSWORD_INVALID);
|
throw new BadRequestException(UserMessages.USER_PASSWORD_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.sendOTP(phoneNumber);
|
await this.sendOTP({ phoneNumber } as SendOtpDto);
|
||||||
}
|
}
|
||||||
|
|
||||||
async verifyOtpAndLogin(dto: VerifyOtpDto) {
|
async verifyOtpAndLogin(dto: VerifyOtpDto) {
|
||||||
|
|||||||
@@ -2,17 +2,25 @@ import { CACHE_MANAGER } from '@nestjs/cache-manager';
|
|||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { ConfigService } from '@nestjs/config';
|
import { ConfigService } from '@nestjs/config';
|
||||||
import type { Cache } from 'cache-manager';
|
import type { Cache } from 'cache-manager';
|
||||||
|
import { metadata } from 'reflect-metadata/no-conflict';
|
||||||
import { OTPMessages } from 'src/common/enums/messages.enum';
|
import { OTPMessages } from 'src/common/enums/messages.enum';
|
||||||
|
|
||||||
interface OtpCache {
|
interface OtpCache<T = any> {
|
||||||
code: string;
|
code: string;
|
||||||
tries: number;
|
tries: number;
|
||||||
|
metadata: T; // holds role, etc...
|
||||||
}
|
}
|
||||||
|
|
||||||
|
type OtpParams<T extends Record<string, any> = {}> = T & {
|
||||||
|
phoneNumber: string;
|
||||||
|
};
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class OtpService {
|
export class OtpService {
|
||||||
private readonly TTL: number;
|
private readonly TTL: number;
|
||||||
|
|
||||||
|
private readonly keyTemplate: (keyof any)[] = ['role']; // could add more later if necessary
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(CACHE_MANAGER)
|
@Inject(CACHE_MANAGER)
|
||||||
private readonly cache: Cache,
|
private readonly cache: Cache,
|
||||||
@@ -21,54 +29,71 @@ export class OtpService {
|
|||||||
this.TTL = this.configService.getOrThrow<number>('CACHE_TTL');
|
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}`;
|
return `otp:${phoneNumber}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
async generate(phoneNumber: string): Promise<string> {
|
async generate<T extends Record<string, any>>(
|
||||||
const code = Math.floor(100000 + Math.random() * 900000).toString();
|
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,
|
code,
|
||||||
tries: 0,
|
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;
|
return code;
|
||||||
}
|
}
|
||||||
|
|
||||||
async verify(phoneNumber: string, code: string): Promise<boolean> {
|
async verify<T extends Record<string, any>>(
|
||||||
const otp = await this.cache.get<OtpCache>(this.getKey(phoneNumber));
|
params: OtpParams<T>,
|
||||||
|
code: string,
|
||||||
|
): Promise<boolean> {
|
||||||
|
const { phoneNumber, ...metadata } = params;
|
||||||
|
const key = this.buildKey(phoneNumber, metadata);
|
||||||
|
|
||||||
if (!otp) {
|
const otp = await this.cache.get<OtpCache<T>>(key);
|
||||||
throw new BadRequestException(OTPMessages.OTP_EXPIRED);
|
|
||||||
}
|
if (!otp) throw new BadRequestException(OTPMessages.OTP_EXPIRED);
|
||||||
|
|
||||||
if (otp.tries >= 5) {
|
if (otp.tries >= 5) {
|
||||||
await this.cache.del(this.getKey(phoneNumber));
|
await this.cache.del(key);
|
||||||
|
|
||||||
throw new BadRequestException(OTPMessages.OTP_TOO_MANY_TRIES);
|
throw new BadRequestException(OTPMessages.OTP_TOO_MANY_TRIES);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (otp.code !== code) {
|
if (otp.code !== code) {
|
||||||
otp.tries++;
|
otp.tries++;
|
||||||
|
await this.cache.set(key, otp, this.TTL);
|
||||||
await this.cache.set(this.getKey(phoneNumber), otp, this.TTL);
|
|
||||||
|
|
||||||
throw new BadRequestException(OTPMessages.OTP_INVALID);
|
throw new BadRequestException(OTPMessages.OTP_INVALID);
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.cache.del(this.getKey(phoneNumber));
|
await this.cache.del(key);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
async delete(phoneNumber: string): Promise<void> {
|
async delete<T extends Record<string, any>>(
|
||||||
await this.cache.del(this.getKey(phoneNumber));
|
params: OtpParams<T>,
|
||||||
|
): Promise<void> {
|
||||||
|
const { phoneNumber, ...metadata } = params;
|
||||||
|
await this.cache.del(this.buildKey(phoneNumber, metadata));
|
||||||
}
|
}
|
||||||
|
|
||||||
async exists(phoneNumber: string): Promise<boolean> {
|
async exists<T extends Record<string, any>>(
|
||||||
return !!(await this.cache.get(this.getKey(phoneNumber)));
|
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