diff --git a/libs/common/src/helper/differenceInSeconds.ts b/libs/common/src/helper/differenceInSeconds.ts new file mode 100644 index 0000000..9f05d67 --- /dev/null +++ b/libs/common/src/helper/differenceInSeconds.ts @@ -0,0 +1,4 @@ +export function differenceInSeconds(date1: Date, date2: Date): number { + const diffInMilliseconds = date1.getTime() - date2.getTime(); + return Math.floor(diffInMilliseconds / 1000); +} diff --git a/libs/common/src/modules/user/entities/user.entity.ts b/libs/common/src/modules/user/entities/user.entity.ts index 11dcd5c..acc9ad7 100644 --- a/libs/common/src/modules/user/entities/user.entity.ts +++ b/libs/common/src/modules/user/entities/user.entity.ts @@ -1,4 +1,11 @@ -import { Column, Entity, ManyToOne, OneToMany, Unique } from 'typeorm'; +import { + Column, + DeleteDateColumn, + Entity, + ManyToOne, + OneToMany, + Unique, +} from 'typeorm'; import { UserDto, UserNotificationDto, @@ -155,6 +162,9 @@ export class UserOtpEntity extends AbstractEntity { }) type: OtpType; + @DeleteDateColumn({ nullable: true }) + deletedAt?: Date; + constructor(partial: Partial) { super(); Object.assign(this, partial); diff --git a/src/auth/dtos/user-otp.dto.ts b/src/auth/dtos/user-otp.dto.ts index bab47c8..043eebc 100644 --- a/src/auth/dtos/user-otp.dto.ts +++ b/src/auth/dtos/user-otp.dto.ts @@ -1,6 +1,12 @@ import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { ApiProperty } from '@nestjs/swagger'; -import { IsEmail, IsEnum, IsNotEmpty, IsString } from 'class-validator'; +import { + IsEmail, + IsEnum, + IsNotEmpty, + IsOptional, + IsString, +} from 'class-validator'; export class UserOtpDto { @ApiProperty() @@ -12,6 +18,11 @@ export class UserOtpDto { @IsEnum(OtpType) @IsNotEmpty() type: OtpType; + + @ApiProperty() + @IsOptional() + @IsString() + regionUuid?: string; } export class VerifyOtpDto extends UserOtpDto { diff --git a/src/auth/services/user-auth.service.ts b/src/auth/services/user-auth.service.ts index 08d0c42..e17f599 100644 --- a/src/auth/services/user-auth.service.ts +++ b/src/auth/services/user-auth.service.ts @@ -18,6 +18,8 @@ import { EmailService } from '../../../libs/common/src/util/email.service'; import { OtpType } from '../../../libs/common/src/constants/otp-type.enum'; import { UserEntity } from '../../../libs/common/src/modules/user/entities/user.entity'; import * as argon2 from 'argon2'; +import { differenceInSeconds } from '@app/common/helper/differenceInSeconds'; +import { LessThan, MoreThan } from 'typeorm'; @Injectable() export class UserAuthService { @@ -114,7 +116,7 @@ export class UserAuthService { async deleteUser(uuid: string) { const user = await this.findOneById(uuid); if (!user) { - throw new BadRequestException('User does not found'); + throw new BadRequestException('User not found'); } return await this.userRepository.update({ uuid }, { isActive: false }); } @@ -124,7 +126,55 @@ export class UserAuthService { } async generateOTP(data: UserOtpDto): Promise { - await this.otpRepository.delete({ email: data.email, type: data.type }); + const threeDaysAgo = new Date(); + threeDaysAgo.setDate(threeDaysAgo.getDate() - 3); + const userExists = await this.userRepository.exists({ + where: { + region: data.regionUuid + ? { + uuid: data.regionUuid, + } + : undefined, + email: data.email, + isUserVerified: true, + }, + }); + if (!userExists) { + throw new BadRequestException('User not found'); + } + await this.otpRepository.softDelete({ email: data.email, type: data.type }); + await this.otpRepository.delete({ + email: data.email, + type: data.type, + createdAt: LessThan(threeDaysAgo), + }); + const countOfOtp = await this.otpRepository.count({ + withDeleted: true, + where: { + email: data.email, + type: data.type, + createdAt: MoreThan(threeDaysAgo), + }, + }); + const lastOtp = await this.otpRepository.findOne({ + where: { email: data.email, type: data.type }, + order: { createdAt: 'DESC' }, + withDeleted: true, + }); + const cooldown = 30 * Math.pow(2, countOfOtp - 1); + if (lastOtp) { + const now = new Date(); + const timeSinceLastOtp = differenceInSeconds(now, lastOtp.createdAt); + + if (timeSinceLastOtp < cooldown) { + throw new BadRequestException({ + message: `Please wait ${cooldown - timeSinceLastOtp} more seconds before requesting a new OTP.`, + data: { + cooldown: cooldown - timeSinceLastOtp, + }, + }); + } + } const otpCode = Math.floor(100000 + Math.random() * 900000).toString(); const expiryTime = new Date(); expiryTime.setMinutes(expiryTime.getMinutes() + 1);