103 lines
2.6 KiB
TypeScript
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()),
|
|
});
|
|
}
|
|
}
|