242 lines
6.8 KiB
TypeScript
242 lines
6.8 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 = 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();
|
|
}
|
|
}
|