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