Files
heala-backend/src/modules/otp/providers/otp.service.ts
T

103 lines
2.6 KiB
TypeScript

import {
HttpStatus,
Injectable,
RequestTimeoutException,
UnauthorizedException,
} from '@nestjs/common';
import { LessThan, MoreThanOrEqual, Repository } from 'typeorm';
import { OTP } from '../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<OTP>,
/**
* 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<string> {
// 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<OTP> {
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<void> {
await this.otpRepository.delete({
expiresAt: LessThan(new Date()),
});
}
}