diff --git a/.env.sample b/.env.sample index 9815d27..1d85cab 100644 --- a/.env.sample +++ b/.env.sample @@ -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 diff --git a/src/api/api.module.ts b/src/api/api.module.ts index b43394c..0e4f234 100644 --- a/src/api/api.module.ts +++ b/src/api/api.module.ts @@ -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, @@ -40,7 +41,17 @@ const otpServiceFactory = { }, inject: [], }.useFactory(config.get('RAJAI_USERNAME'), config.get('RAJAI_PASSWORD'), config.get('RAJAI_BASEURL')); - } + } else if (config.get('SMS_ADAPTER_TYPE') == 'FONADA') { + factory = { + provide: 'OtpService', + useFactory: () => { + return new FonadaService( + config + ); + }, + inject: [], + }.useFactory(); + } else { factory = { provide: 'OtpService', diff --git a/src/api/sms/fonada/fonada.service.ts b/src/api/sms/fonada/fonada.service.ts new file mode 100644 index 0000000..fcea254 --- /dev/null +++ b/src/api/sms/fonada/fonada.service.ts @@ -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('FONADA_SERVICE_URL'); + this.auth = { + userid: configService.get('FONADA_USERNAME'), + password: configService.get('FONADA_PASSWORD'), + } + this.httpClient = got; + } + + send(data: SMSData): Promise { + 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 { + 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('SMS_TOTP_SECRET')}${phone}` + } + + private doOTPRequest(data: SMSData): Promise { + let otp = ''; + try { + otp = speakeasy.totp({ + secret: this.getTotpSecret(data.phone), + encoding: 'base32', + step: this.configService.get('SMS_TOTP_EXPIRY'), + digits: 4, + }); + } catch (error) { + throw new HttpException('TOTP generation failed!', 500); + } + + const payload = this.configService.get('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('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 { + 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 { + 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('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); + } + } + } + \ No newline at end of file