pos-be/src/modules/configuration/otp-verification/data/services/otp-verification.service.ts

175 lines
5.1 KiB
TypeScript

import { BadRequestException, Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { OtpVerificationModel } from '../models/otp-verification.model';
import {
OtpRequestEntity,
OtpVerificationEntity,
OtpVerifierEntity,
// OtpVerifierEntity,
OtpVerifyEntity,
} from '../../domain/entities/otp-verification.entity';
import * as moment from 'moment';
import { OtpService } from 'src/core/helpers/otp/otp-service';
import { TABLE_NAME } from 'src/core/strings/constants/table.constants';
import { WhatsappService } from 'src/services/whatsapp/whatsapp.service';
import { OtpVerifierModel } from '../models/otp-verifier.model';
@Injectable()
export class OtpVerificationService {
constructor(
@InjectRepository(OtpVerificationModel)
private readonly otpVerificationRepo: Repository<OtpVerificationModel>,
@InjectRepository(OtpVerifierModel)
private readonly otpVerifierRepo: Repository<OtpVerifierModel>,
) {}
private generateOtpExpiration(minutes = 5): number {
return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time
}
private generateResendAvailableAt(seconds = 90): number {
return moment().add(seconds, 'seconds').valueOf(); // epoch millis
}
private generateTimestamp(): number {
return moment().valueOf(); // epoch millis verification time (now)
}
async requestOTP(payload: OtpRequestEntity) {
const otpService = new OtpService({ length: 4 });
const otpCode = otpService.generateSecureOTP();
const dateNow = this.generateTimestamp();
const expiredAt = this.generateOtpExpiration();
//TODO implementation from auth
const creator = { id: null, name: null };
const newOtp: OtpVerificationEntity = {
otp_code: otpCode,
action_type: payload.action_type,
target_id: payload.target_id,
reference: payload.reference,
source: payload.source,
is_used: false,
is_replaced: false,
expired_at: expiredAt,
creator_id: creator.id,
creator_name: creator.name,
created_at: dateNow,
verified_at: null,
editor_id: creator.id,
editor_name: creator.name,
updated_at: dateNow,
};
const activeOTP = await this.getActiveOtp(
payload.target_id ?? payload.reference,
);
if (activeOTP) {
const createdAtMoment = moment(Number(activeOTP.created_at));
const nowMoment = moment(Number(dateNow));
const diffSeconds = nowMoment.diff(createdAtMoment, 'seconds');
if (diffSeconds < 90) {
throw new BadRequestException(
'An active OTP request was made recently. Please try again later.',
);
} else {
// Update data is_replaced on database
this.otpVerificationRepo.save({
...activeOTP,
is_replaced: true,
});
}
}
// save otp to database
await this.otpVerificationRepo.save(newOtp);
const verifiers: OtpVerifierEntity[] = await this.otpVerifierRepo.find();
const notificationService = new WhatsappService();
verifiers.map((v) => {
notificationService.sendOtpNotification({
phone: v.phone_number,
code: otpCode,
});
});
return {
message: `OTP has been sent to the admin's WhatsApp.`,
updated_at: expiredAt,
resend_available_at: this.generateResendAvailableAt(),
};
}
async verifyOTP(payload: OtpVerifyEntity) {
const { otp_code, action_type, target_id, reference, source } = payload;
const dateNow = this.generateTimestamp();
if (!target_id && !reference) {
throw new BadRequestException(
'Either target_id or reference must be provided.',
);
}
// Build a where condition with OR between target_id and reference
const otp = await this.otpVerificationRepo.findOne({
where: [
{
otp_code,
action_type,
target_id,
source,
is_used: false,
is_replaced: false,
},
{
otp_code,
action_type,
reference,
source,
is_used: false,
is_replaced: false,
},
],
});
if (!otp) {
throw new BadRequestException('Invalid or expired OTP.');
} else if (otp.expired_at <= dateNow) {
throw new BadRequestException('OTP has expired.');
}
otp.is_used = true;
otp.verified_at = dateNow;
// update otp to database
await this.otpVerificationRepo.save(otp);
return { message: 'OTP verified successfully.' };
}
async getActiveOtp(payload: string) {
const now = this.generateTimestamp();
const tableName = TABLE_NAME.OTP_VERIFICATIONS;
return this.otpVerificationRepo
.createQueryBuilder(tableName)
.where(
`(${tableName}.target_id = :payload OR ${tableName}.reference = :payload)
AND ${tableName}.is_used = false
AND ${tableName}.is_replaced = false
AND ${tableName}.expired_at > :now`,
{ payload, now },
)
.orderBy(
`CASE WHEN ${tableName}.target_id = :payload THEN 0 ELSE 1 END`,
'ASC',
)
.getOne();
}
}