import { HttpStatus, Injectable, RequestTimeoutException, UnauthorizedException, } from '@nestjs/common'; import { LessThan, MoreThanOrEqual, Repository } from 'typeorm'; import { OTP } from '../entities/otp.entity'; import { InjectRepository } from '@nestjs/typeorm'; import { Cron } from '@nestjs/schedule'; import { HashingProvider } from '@/common/modules/hashing/providers/hashing.provider'; import { AppException } from '@/common/exceptions/app.exception'; @Injectable() export class OtpService { constructor( /** * Inject OTP Repository */ @InjectRepository(OTP) private readonly otpRepository: Repository, /** * Inject Hashing Provider */ private readonly hashingProvider: HashingProvider, ) {} /** * Generate OTP 5-digit code */ private generateOTPCode(): string { return Math.floor(10_000 + Math.random() * 90_000).toString(); } /** * Create new OTP record */ public async createOTP(phone: string): Promise { // OTP request Rate limit const lastOTP = await this.otpRepository.findOne({ where: { phone }, order: { createdAt: 'DESC' }, }); if (lastOTP && Date.now() - lastOTP.createdAt.getTime() < 60_000) { throw new AppException( 'common.errors.tooManyRequests', HttpStatus.TOO_MANY_REQUESTS, { minutes: 1 }, ); } const code = this.generateOTPCode(); const hashedCode = await this.hashingProvider.hash(code); const otp = this.otpRepository.create({ phone, code: hashedCode }); await this.otpRepository.save(otp); return code; } /** * Verify OTP */ public async verifyOTP(phone: string, code: string): Promise { let otp: OTP | null = null; try { otp = await this.otpRepository.findOne({ where: { phone, used: false, expiresAt: MoreThanOrEqual(new Date()) }, order: { createdAt: 'DESC' }, }); } catch (err) { throw new RequestTimeoutException('common.errors.requestTimeout', { cause: err, description: 'Error connecting to the database', }); } if (!otp) throw new UnauthorizedException('auth.errors.invalidOTP'); const isOTPCorrect = await this.hashingProvider.compare(code, otp.code); if (!isOTPCorrect) { throw new UnauthorizedException('auth.errors.invalidOTP'); } otp.used = true; await this.otpRepository.save(otp); return otp; } @Cron('0 */10 * * * *') public async cleanupExpired(): Promise { await this.otpRepository.delete({ expiresAt: LessThan(new Date()), }); } }