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, @InjectRepository(OtpVerifierModel) private readonly otpVerifierRepo: Repository, ) {} private generateOtpExpiration(minutes = 5): number { return moment().add(minutes, 'minutes').valueOf(); // epoch millis expired time } private generateResendAvailableAt(seconds = 60): number { return moment().add(seconds, 'seconds').valueOf(); // epoch millis } private generateTimestamp(): number { return moment().valueOf(); // epoch millis verification time (now) } private generateOTPMsgTemplate(payload) { const { userRequest, newOtp } = payload; const header = newOtp.action_type.split('_').join(' '); const otpCode = newOtp?.otp_code; const username = userRequest?.username; const otpType = newOtp.action_type .split('_') .map((word) => word.charAt(0).toUpperCase() + word.slice(1).toLowerCase()) .join(' '); return { name: 'general_flow', language: { code: 'id' }, components: [ { type: 'header', parameters: [ { type: 'text', parameter_name: 'header', text: header, }, ], }, { type: 'body', parameters: [ { type: 'text', parameter_name: 'name', text: username, }, { type: 'text', parameter_name: 'code', text: otpCode, }, { type: 'text', parameter_name: 'type', text: otpType, }, ], }, { type: 'footer', parameters: [ { type: 'text', text: 'Kode berlaku selama 5 menit.', }, ], }, ], }; } async requestOTP(payload: OtpRequestEntity, req: any) { const otpService = new OtpService({ length: 4 }); const otpCode = otpService.generateSecureOTP(); const dateNow = this.generateTimestamp(); const expiredAt = this.generateOtpExpiration(); const userRequest = req?.user; 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: userRequest?.id, creator_name: userRequest?.name, created_at: dateNow, verified_at: null, editor_id: userRequest?.id, editor_name: userRequest?.name, updated_at: dateNow, }; const activeOTP = await this.getActiveOtp( payload.target_id ? 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'); const isProduction = process.env.NODE_ENV === 'true'; if (diffSeconds < 60 && isProduction) { 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.sendTemplateMessage({ phone: v.phone_number, templateMsg: this.generateOTPMsgTemplate({ userRequest, newOtp }), }); }); return { message: `OTP has been sent to the admin's WhatsApp.`, updated_at: expiredAt, resend_available_at: this.generateResendAvailableAt(), }; } async verifyOTP(payload: OtpVerifyEntity, req: any) { const userRequest = req?.user; 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.', ); } let otp: any; // Build a where condition with OR between target_id and reference if (target_id) { otp = await this.otpVerificationRepo.findOne({ where: { otp_code, action_type, target_id, source, is_used: false, is_replaced: false, }, }); } else if (reference) { otp = await this.otpVerificationRepo.findOne({ where: { 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; otp.editor_id = userRequest?.id; otp.editor_name = userRequest?.name; otp.updated_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(); } }