Skip to content

Commit

Permalink
Add Fonada SMS service integration and update .env.sample
Browse files Browse the repository at this point in the history
  • Loading branch information
Manasasidd committed Jan 17, 2025
1 parent f54c259 commit f6c0889
Show file tree
Hide file tree
Showing 3 changed files with 247 additions and 1 deletion.
7 changes: 7 additions & 0 deletions .env.sample
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,13 @@ CDAC_SERVICE_URL=
CDAC_OTP_TEMPLATE_ID="123456"
CDAC_OTP_TEMPLATE="Respected User, The OTP to reset password for %phone% is %code%."

# FONADA
FONADA_SERVICE_URL=
FONADA_OTP_TEMPLATE=
FONADA_OTP_TEMPLATE_ID=
FONADA_USERNAME=
FONADA_PASSWORD=

# SMS Adapter
SMS_ADAPTER_TYPE= # CDAC or GUPSHUP or RAJAI
SMS_TOTP_SECRET= # any random string, needed for CDAC
Expand Down
13 changes: 12 additions & 1 deletion src/api/api.module.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { CdacService } from './sms/cdac/cdac.service';
import { RajaiOtpService } from '../user/sms/rajaiOtpService/rajaiOtpService.service';
import { GupshupWhatsappService } from './sms/gupshupWhatsapp/gupshupWhatsapp.service';
import { TelemetryService } from 'src/telemetry/telemetry.service';
import { FonadaService } from './sms/fonada/fonada.service';

const otpServiceFactory = {
provide: OtpService,
Expand All @@ -40,7 +41,17 @@ const otpServiceFactory = {
},
inject: [],
}.useFactory(config.get('RAJAI_USERNAME'), config.get('RAJAI_PASSWORD'), config.get('RAJAI_BASEURL'));
}
} else if (config.get<string>('SMS_ADAPTER_TYPE') == 'FONADA') {
factory = {
provide: 'OtpService',
useFactory: () => {
return new FonadaService(
config
);
},
inject: [],
}.useFactory();
}
else {
factory = {
provide: 'OtpService',
Expand Down
228 changes: 228 additions & 0 deletions src/api/sms/fonada/fonada.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,228 @@
import {
OTPResponse,
SMS,
SMSData,
SMSError,
SMSProvider,
SMSResponse,
SMSResponseStatus,
SMSType,
TrackResponse,
} from '../sms.interface';

import { HttpException, Injectable } from '@nestjs/common';
import { SmsService } from '../sms.service';
import { ConfigService } from '@nestjs/config';
import got, {Got} from 'got';
import * as speakeasy from 'speakeasy';

@Injectable()
export class FonadaService extends SmsService implements SMS {
baseURL: string;
path = '';
data: SMSData;
httpClient: Got;
auth: any;
constructor(
private configService: ConfigService,
) {
super();
this.baseURL = configService.get<string>('FONADA_SERVICE_URL');
this.auth = {
userid: configService.get<string>('FONADA_USERNAME'),
password: configService.get<string>('FONADA_PASSWORD'),
}
this.httpClient = got;
}

send(data: SMSData): Promise<SMSResponse> {
if (!data) {
throw new Error('Data cannot be empty');
}
this.data = data;
if (this.data.type === SMSType.otp) return this.doOTPRequest(data);
else return this.doRequest();
}

track(data: SMSData): Promise<SMSResponse> {
if (!data) {
throw new Error('Data cannot be null');
}
this.data = data;
if (this.data.type === SMSType.otp) return this.verifyOTP(data);
else return this.doRequest();
}

private getTotpSecret(phone): string {
return `${this.configService.get<string>('SMS_TOTP_SECRET')}${phone}`
}

private doOTPRequest(data: SMSData): Promise<any> {
let otp = '';
try {
otp = speakeasy.totp({
secret: this.getTotpSecret(data.phone),
encoding: 'base32',
step: this.configService.get<string>('SMS_TOTP_EXPIRY'),
digits: 4,
});
} catch (error) {
throw new HttpException('TOTP generation failed!', 500);
}

const payload = this.configService.get<string>('FONADA_OTP_TEMPLATE')
.replace('%phone%', data.phone)
.replace('%code%', otp + '');
const params = new URLSearchParams({
username:this.auth.userid,
password:this.auth.password,
unicode:"true",
from:"CMPTKM",
to:data.phone,
text:payload,
dltContentId:this.configService.get<string>('FONADA_OTP_TEMPLATE_ID'),
});
this.path = '/fe/api/v1/send'
const url = `${this.baseURL}${this.path}?${params.toString()}`;

const status: OTPResponse = {} as OTPResponse;
status.provider = SMSProvider.cdac;
status.phone = data.phone;

// noinspection DuplicatedCode
return this.httpClient.get(url, {})
.then((response): OTPResponse => {
status.networkResponseCode = 200;
const r = this.parseResponse(response.body);
status.messageID = r.messageID;
status.error = r.error;
status.providerResponseCode = r.providerResponseCode;
status.providerSuccessResponse = r.providerSuccessResponse;
status.status = r.status;
return status;
})
.catch((e: Error): OTPResponse => {
const error: SMSError = {
errorText: `Uncaught Exception :: ${e.message}`,
errorCode: 'CUSTOM ERROR',
};
status.networkResponseCode = 200;
status.messageID = null;
status.error = error;
status.providerResponseCode = null;
status.providerSuccessResponse = null;
status.status = SMSResponseStatus.failure;
return status;
});
}

doRequest(): Promise<SMSResponse> {
throw new Error('Method not implemented.');
}

parseResponse(response: any) {
response = JSON.parse(response);
try {
if (response.state == 'SUBMIT_ACCEPTED') {
return {
providerResponseCode: response.state,
status: SMSResponseStatus.success,
messageID: response.transactionId,
error: null,
providerSuccessResponse: null,
};
} else {
const error: SMSError = {
errorText: response.description,
errorCode: response.state,
};
return {
providerResponseCode: response.state,
status: SMSResponseStatus.failure,
messageID: response.transactionId,
error,
providerSuccessResponse: null,
};
}
} catch (e) {
const error: SMSError = {
errorText: `CDAC response could not be parsed :: ${e.message}; Provider Response - ${response}`,
errorCode: 'CUSTOM ERROR',
};
return {
providerResponseCode: null,
status: SMSResponseStatus.failure,
messageID: null,
error,
providerSuccessResponse: null,
};
}
}

verifyOTP(data: SMSData): Promise<TrackResponse> {
if(
process.env.ALLOW_DEFAULT_OTP === 'true' &&
process.env.DEFAULT_OTP_USERS
){
if(JSON.parse(process.env.DEFAULT_OTP_USERS).indexOf(data.phone)!=-1){
if(data.params.otp == process.env.DEFAULT_OTP) {
return new Promise(resolve => {
const status: TrackResponse = {} as TrackResponse;
status.provider = SMSProvider.cdac;
status.phone = data.phone;
status.networkResponseCode = 200;
status.messageID = Date.now() + '';
status.error = null;
status.providerResponseCode = null;
status.providerSuccessResponse = 'OTP matched.';
status.status = SMSResponseStatus.success;
resolve(status);
});
}
}
}

let verified = false;
try {
verified = speakeasy.totp.verify({
secret: this.getTotpSecret(data.phone.replace(/^\+\d{1,3}[-\s]?/, '')),
encoding: 'base32',
token: data.params.otp,
step: this.configService.get<string>('SMS_TOTP_EXPIRY'),
digits: 4,
});
if (verified) {
return new Promise(resolve => {
const status: TrackResponse = {} as TrackResponse;
status.provider = SMSProvider.cdac;
status.phone = data.phone;
status.messageID = '';
status.error = null;
status.providerResponseCode = null;
status.providerSuccessResponse = null;
status.status = SMSResponseStatus.success;
resolve(status);
});
} else {
return new Promise(resolve => {
const status: TrackResponse = {} as TrackResponse;
status.provider = SMSProvider.cdac;
status.phone = data.phone;
status.networkResponseCode = 200;
status.messageID = '';
status.error = {
errorText: 'Invalid or expired OTP.',
errorCode: '400'
};
status.providerResponseCode = '400';
status.providerSuccessResponse = null;
status.status = SMSResponseStatus.failure;
resolve(status);
});
}
} catch(error) {
throw new HttpException(error, 500);
}
}
}

0 comments on commit f6c0889

Please sign in to comment.