1+ import {
2+ OTPResponse ,
3+ SMS ,
4+ SMSData ,
5+ SMSError ,
6+ SMSProvider ,
7+ SMSResponse ,
8+ SMSResponseStatus ,
9+ SMSType ,
10+ TrackResponse ,
11+ } from '../sms.interface' ;
12+
13+ import { HttpException , Injectable } from '@nestjs/common' ;
14+ import { SmsService } from '../sms.service' ;
15+ import { ConfigService } from '@nestjs/config' ;
16+ import got , { Got } from 'got' ;
17+ import * as speakeasy from 'speakeasy' ;
18+
19+ @Injectable ( )
20+ export class FonadaService extends SmsService implements SMS {
21+ baseURL : string ;
22+ path = '' ;
23+ data : SMSData ;
24+ httpClient : Got ;
25+ auth : any ;
26+ constructor (
27+ private configService : ConfigService ,
28+ ) {
29+ super ( ) ;
30+ this . baseURL = configService . get < string > ( 'FONADA_SERVICE_URL' ) ;
31+ this . auth = {
32+ userid : configService . get < string > ( 'FONADA_USERNAME' ) ,
33+ password : configService . get < string > ( 'FONADA_PASSWORD' ) ,
34+ }
35+ this . httpClient = got ;
36+ }
37+
38+ send ( data : SMSData ) : Promise < SMSResponse > {
39+ if ( ! data ) {
40+ throw new Error ( 'Data cannot be empty' ) ;
41+ }
42+ this . data = data ;
43+ if ( this . data . type === SMSType . otp ) return this . doOTPRequest ( data ) ;
44+ else return this . doRequest ( ) ;
45+ }
46+
47+ track ( data : SMSData ) : Promise < SMSResponse > {
48+ if ( ! data ) {
49+ throw new Error ( 'Data cannot be null' ) ;
50+ }
51+ this . data = data ;
52+ if ( this . data . type === SMSType . otp ) return this . verifyOTP ( data ) ;
53+ else return this . doRequest ( ) ;
54+ }
55+
56+ private getTotpSecret ( phone ) : string {
57+ return `${ this . configService . get < string > ( 'SMS_TOTP_SECRET' ) } ${ phone } `
58+ }
59+
60+ private doOTPRequest ( data : SMSData ) : Promise < any > {
61+ let otp = '' ;
62+ try {
63+ otp = speakeasy . totp ( {
64+ secret : this . getTotpSecret ( data . phone ) ,
65+ encoding : 'base32' ,
66+ step : this . configService . get < string > ( 'SMS_TOTP_EXPIRY' ) ,
67+ digits : 4 ,
68+ } ) ;
69+ } catch ( error ) {
70+ throw new HttpException ( 'TOTP generation failed!' , 500 ) ;
71+ }
72+
73+ const payload = this . configService . get < string > ( 'FONADA_OTP_TEMPLATE' )
74+ . replace ( '%phone%' , data . phone )
75+ . replace ( '%code%' , otp + '' ) ;
76+ const params = new URLSearchParams ( {
77+ username :this . auth . userid ,
78+ password :this . auth . password ,
79+ unicode :"true" ,
80+ from :"CMPTKM" ,
81+ to :data . phone ,
82+ text :payload ,
83+ dltContentId :this . configService . get < string > ( 'FONADA_OTP_TEMPLATE_ID' ) ,
84+ } ) ;
85+ this . path = '/fe/api/v1/send'
86+ const url = `${ this . baseURL } ${ this . path } ?${ params . toString ( ) } ` ;
87+
88+ const status : OTPResponse = { } as OTPResponse ;
89+ status . provider = SMSProvider . cdac ;
90+ status . phone = data . phone ;
91+
92+ // noinspection DuplicatedCode
93+ return this . httpClient . get ( url , { } )
94+ . then ( ( response ) : OTPResponse => {
95+ status . networkResponseCode = 200 ;
96+ const r = this . parseResponse ( response . body ) ;
97+ status . messageID = r . messageID ;
98+ status . error = r . error ;
99+ status . providerResponseCode = r . providerResponseCode ;
100+ status . providerSuccessResponse = r . providerSuccessResponse ;
101+ status . status = r . status ;
102+ return status ;
103+ } )
104+ . catch ( ( e : Error ) : OTPResponse => {
105+ const error : SMSError = {
106+ errorText : `Uncaught Exception :: ${ e . message } ` ,
107+ errorCode : 'CUSTOM ERROR' ,
108+ } ;
109+ status . networkResponseCode = 200 ;
110+ status . messageID = null ;
111+ status . error = error ;
112+ status . providerResponseCode = null ;
113+ status . providerSuccessResponse = null ;
114+ status . status = SMSResponseStatus . failure ;
115+ return status ;
116+ } ) ;
117+ }
118+
119+ doRequest ( ) : Promise < SMSResponse > {
120+ throw new Error ( 'Method not implemented.' ) ;
121+ }
122+
123+ parseResponse ( response : any ) {
124+ response = JSON . parse ( response ) ;
125+ try {
126+ if ( response . state == 'SUBMIT_ACCEPTED' ) {
127+ return {
128+ providerResponseCode : response . state ,
129+ status : SMSResponseStatus . success ,
130+ messageID : response . transactionId ,
131+ error : null ,
132+ providerSuccessResponse : null ,
133+ } ;
134+ } else {
135+ const error : SMSError = {
136+ errorText : response . description ,
137+ errorCode : response . state ,
138+ } ;
139+ return {
140+ providerResponseCode : response . state ,
141+ status : SMSResponseStatus . failure ,
142+ messageID : response . transactionId ,
143+ error,
144+ providerSuccessResponse : null ,
145+ } ;
146+ }
147+ } catch ( e ) {
148+ const error : SMSError = {
149+ errorText : `CDAC response could not be parsed :: ${ e . message } ; Provider Response - ${ response } ` ,
150+ errorCode : 'CUSTOM ERROR' ,
151+ } ;
152+ return {
153+ providerResponseCode : null ,
154+ status : SMSResponseStatus . failure ,
155+ messageID : null ,
156+ error,
157+ providerSuccessResponse : null ,
158+ } ;
159+ }
160+ }
161+
162+ verifyOTP ( data : SMSData ) : Promise < TrackResponse > {
163+ if (
164+ process . env . ALLOW_DEFAULT_OTP === 'true' &&
165+ process . env . DEFAULT_OTP_USERS
166+ ) {
167+ if ( JSON . parse ( process . env . DEFAULT_OTP_USERS ) . indexOf ( data . phone ) != - 1 ) {
168+ if ( data . params . otp == process . env . DEFAULT_OTP ) {
169+ return new Promise ( resolve => {
170+ const status : TrackResponse = { } as TrackResponse ;
171+ status . provider = SMSProvider . cdac ;
172+ status . phone = data . phone ;
173+ status . networkResponseCode = 200 ;
174+ status . messageID = Date . now ( ) + '' ;
175+ status . error = null ;
176+ status . providerResponseCode = null ;
177+ status . providerSuccessResponse = 'OTP matched.' ;
178+ status . status = SMSResponseStatus . success ;
179+ resolve ( status ) ;
180+ } ) ;
181+ }
182+ }
183+ }
184+
185+ let verified = false ;
186+ try {
187+ verified = speakeasy . totp . verify ( {
188+ secret : this . getTotpSecret ( data . phone . replace ( / ^ \+ \d { 1 , 3 } [ - \s ] ? / , '' ) ) ,
189+ encoding : 'base32' ,
190+ token : data . params . otp ,
191+ step : this . configService . get < string > ( 'SMS_TOTP_EXPIRY' ) ,
192+ digits : 4 ,
193+ } ) ;
194+ if ( verified ) {
195+ return new Promise ( resolve => {
196+ const status : TrackResponse = { } as TrackResponse ;
197+ status . provider = SMSProvider . cdac ;
198+ status . phone = data . phone ;
199+ status . messageID = '' ;
200+ status . error = null ;
201+ status . providerResponseCode = null ;
202+ status . providerSuccessResponse = null ;
203+ status . status = SMSResponseStatus . success ;
204+ resolve ( status ) ;
205+ } ) ;
206+ } else {
207+ return new Promise ( resolve => {
208+ const status : TrackResponse = { } as TrackResponse ;
209+ status . provider = SMSProvider . cdac ;
210+ status . phone = data . phone ;
211+ status . networkResponseCode = 200 ;
212+ status . messageID = '' ;
213+ status . error = {
214+ errorText : 'Invalid or expired OTP.' ,
215+ errorCode : '400'
216+ } ;
217+ status . providerResponseCode = '400' ;
218+ status . providerSuccessResponse = null ;
219+ status . status = SMSResponseStatus . failure ;
220+ resolve ( status ) ;
221+ } ) ;
222+ }
223+ } catch ( error ) {
224+ throw new HttpException ( error , 500 ) ;
225+ }
226+ }
227+ }
228+
0 commit comments