Unverified Commit ebd774a4 authored by Abhishek Mishra's avatar Abhishek Mishra Committed by GitHub
Browse files

Merge pull request #74 from choxx/features/51/send-sms-through-cdac

CDAC OTP sending support added (#51)
No related merge requests found
Showing with 247 additions and 81 deletions
+247 -81
......@@ -7,6 +7,16 @@ GUPSHUP_USERNAME="2000xxxxxxx"
GUPSHUP_PASSWORD="password"
GUPSHUP_OTP_TEMPLATE="Hi User! The OTP to reset password for Samagra Shiksha App is %code%."
# CDAC
CDAC_SERVICE_URL=
CDAC_OTP_TEMPLATE_ID="123456"
CDAC_OTP_TEMPLATE="Respected User, The OTP to reset password for %phone% is %code%."
# SMS Adapter
SMS_ADAPTER_TYPE= # CDAC or GUPSHUP
SMS_TOTP_SECRET= # any random string, needed for CDAC
SMS_TOTP_EXPIRY=600 # in seconds, needed for CDAC
# Fusionauth
FUSIONAUTH_APPLICATION_ID="f0ddb3f6-091b-45e4-8c0f-889f89d4f5da"
FUSIONAUTH_SAMARTH_HP_APPLICATION_ID=f18c3f6f-45b8-4928-b978-a9906fd03f22
......@@ -14,6 +24,6 @@ FUSIONAUTH_HP_ADMIN_CONSOLE_APPLICATION_ID=
FUSIONAUTH_BASE_URL="https://auth.samarth.samagra.io"
FUSIONAUTH_API_KEY="bla"
ENCRYPTION_KEY="bla"
FUSIONAUTH_ADMIN_SEARCH_APPLICATION_IDS=["1", "2"] # JSON array of application IDs
FUSIONAUTH_ADMIN_SEARCH_APPLICATION_IDS=["1","2"] # JSON array of application IDs
# APP_abcd3f6f_45b8_4928_b978_a9906fd03f22={"host": "dummy.com", "encryption": {"enabled": true, "key": "veryhardkey"}, "salt": "sampl-salt-for-otp-encrypption"} # the key "application_id" must be underscore(_) separated instead of hyphen(-). Also it must be prefixed with <APP_>
\ No newline at end of file
......@@ -47,6 +47,7 @@
"reflect-metadata": "^0.1.13",
"rimraf": "^3.0.2",
"rxjs": "^7.8.0",
"speakeasy": "^2.0.0",
"swagger-ui-express": "^4.1.6",
"uuid": "^8.3.2"
},
......
......@@ -10,24 +10,35 @@ import { OtpService } from './otp/otp.service';
import { GupshupService } from './sms/gupshup/gupshup.service';
import { SmsService } from './sms/sms.service';
import got from 'got/dist/source';
const gupshupFactory = {
provide: 'OtpService',
useFactory: (username, password, baseUrl) => {
return new GupshupService(
username,
password,
baseUrl,
got,
);
},
inject: [],
};
import { CdacService } from './sms/cdac/cdac.service';
const otpServiceFactory = {
provide: OtpService,
useFactory: (config: ConfigService) => {
return new OtpService(gupshupFactory.useFactory(config.get('GUPSHUP_USERNAME'), config.get('GUPSHUP_PASSWORD'), config.get('GUPSHUP_BASEURL'),));
let factory;
if (config.get<string>('SMS_ADAPTER_TYPE') == 'CDAC') {
factory = {
provide: 'OtpService',
useFactory: () => {
return new CdacService(config);
},
inject: [],
}.useFactory();
} else {
factory = {
provide: 'OtpService',
useFactory: (username, password, baseUrl) => {
return new GupshupService(
username,
password,
baseUrl,
got,
);
},
inject: [],
}.useFactory(config.get('GUPSHUP_USERNAME'), config.get('GUPSHUP_PASSWORD'), config.get('GUPSHUP_BASEURL'));
}
return new OtpService(factory);
},
inject: [ConfigService],
};
......@@ -35,6 +46,7 @@ const otpServiceFactory = {
@Module({
imports: [HttpModule, ConfigModule],
controllers: [ApiController],
providers: [ApiService, FusionauthService, SmsService, otpServiceFactory, QueryGeneratorService, ConfigResolverService]
providers: [ApiService, FusionauthService, SmsService, otpServiceFactory, QueryGeneratorService, ConfigResolverService],
})
export class ApiModule {}
export class ApiModule {
}
import { Test, TestingModule } from '@nestjs/testing';
import { CdacService } from './cdac.service';
import { ConfigService } from '@nestjs/config';
import { OtpService } from '../../otp/otp.service';
const otpServiceFactory = {
provide: OtpService,
useFactory: (config: ConfigService) => {
let factory;
factory = {
provide: 'OtpService',
useFactory: () => {
return new CdacService(config);
},
inject: [],
}.useFactory();
return new OtpService(factory);
},
inject: [ConfigService],
};
describe('CdacService', () => {
let service: CdacService;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [CdacService],
providers: [ConfigService, CdacService, otpServiceFactory],
}).compile();
service = module.get<CdacService>(CdacService);
......@@ -14,5 +32,6 @@ describe('CdacService', () => {
it('should be defined', () => {
expect(service).toBeDefined();
expect(service).toBeDefined();
});
});
import { SMS, SMSResponse } from '../sms.interface';
import {
OTPResponse,
SMS,
SMSData,
SMSError,
SMSProvider,
SMSResponse,
SMSResponseStatus,
SMSType,
TrackResponse,
} from '../sms.interface';
import { Injectable } from '@nestjs/common';
import { HttpException, Injectable } from '@nestjs/common';
import { SmsService } from '../sms.service';
import fetch from 'node-fetch';
import { ConfigService } from '@nestjs/config';
import got, {Got} from 'got';
import * as speakeasy from 'speakeasy';
@Injectable()
export class CdacService extends SmsService implements SMS {
send(id: any): Promise<SMSResponse> {
console.log(id);
throw new Error('Method not implemented.');
baseURL: string;
path = '';
data: SMSData;
httpClient: Got;
constructor(
private configService: ConfigService,
) {
super();
this.baseURL = configService.get<string>('CDAC_SERVICE_URL');
this.httpClient = got;
}
track(id: any): Promise<SMSResponse> {
console.log(id);
throw new Error('Method not implemented.');
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();
}
convertFromUnicodeToText = (message) => {
let finalMessage = '';
console.log(message);
for (let i = 0; i < message.length; i++) {
const ch = message.charCodeAt(i);
const j = ch;
const sss = '&#' + j + ';';
finalMessage = finalMessage + sss;
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'),
});
} catch (error) {
console.log(error);
throw new HttpException('TOTP generation failed!', 500);
}
return finalMessage;
};
sendSingleSMS = async (params) => {
const url = `https://msdgweb.mgov.gov.in/esms/sendsmsrequest?`;
const esc = encodeURIComponent;
const query = Object.keys(params)
.map((k) => esc(k) + '=' + esc(params[k]))
.join('&');
const payload = this.configService.get<string>('CDAC_OTP_TEMPLATE')
.replace('%phone%', data.phone)
.replace('%code%', otp + '');
const params = new URLSearchParams({
message: payload,
mobileNumber: data.phone,
templateid: this.configService.get<string>('CDAC_OTP_TEMPLATE_ID'),
});
this.path = '/api/send_otp_sms'
const url = `${this.baseURL}${this.path}?${params.toString()}`;
// console.log('Sending SMS now');
// console.log(query);
return fetch(url + query, {
method: 'POST',
timeout: 15000,
})
.then((r) => {
// console.log('Request finished for: ');
const text = r.text();
return text;
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) => {
console.log(e);
return '498, ';
.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;
});
};
}
parseSMSResponse = (response) => {
// console.log(response);
let s;
// Case for error => 'ERROR : 406 Invalid mobile number\r\n'
try {
s = response.split(':');
return {
responseCode: parseInt(s[1].trim().split(' ')[0]),
messageID: undefined,
};
} catch (e) {}
doRequest(): Promise<SMSResponse> {
throw new Error('Method not implemented.');
}
// Case for normal response => '402,MsgID = 070820191565174573847hpgovt-hpssa\r\n'
s = response.split(',');
parseResponse(response: string) {
try {
const messageID = s[1].split('=')[1].trim();
return {
responseCode: parseInt(s[0]),
messageID: messageID,
};
const responseCode: string = response.slice(0, 3);
if (responseCode === '402') {
return {
providerResponseCode: null,
status: SMSResponseStatus.success,
messageID: response.slice(12, -1),
error: null,
providerSuccessResponse: null,
};
} else {
const error: SMSError = {
errorText: 'CDAC Error',
errorCode: responseCode,
};
return {
providerResponseCode: responseCode,
status: SMSResponseStatus.failure,
messageID: null,
error,
providerSuccessResponse: null,
};
}
} catch (e) {
const error: SMSError = {
errorText: `CDAC response could not be parsed :: ${e.message}; Provider Response - ${response}`,
errorCode: 'CUSTOM ERROR',
};
return {
responseCode: parseInt(s[0]),
messageID: undefined,
providerResponseCode: null,
status: SMSResponseStatus.failure,
messageID: null,
error,
providerSuccessResponse: null,
};
}
};
}
verifyOTP(data: SMSData): Promise<TrackResponse> {
let verified = false;
try {
verified = speakeasy.totp.verify({
secret: this.getTotpSecret(data.phone),
encoding: 'base32',
token: data.params.otp,
step: this.configService.get<string>('SMS_TOTP_EXPIRY'),
});
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);
}
}
}
import {
ExecutionContext,
Injectable,
UnauthorizedException,
} from '@nestjs/common';
import { AuthGuard, IAuthGuard } from '@nestjs/passport';
import { JwtService } from '@nestjs/jwt'
import { Reflector } from '@nestjs/core';
@Injectable()
......@@ -36,8 +34,8 @@ import {
return isAllowed;
}
// noinspection JSUnusedLocalSymbols
handleRequest(err, user, info) {
console.log({handleRequest: info, error: err, user: user})
return user;
}
}
\ No newline at end of file
......@@ -2040,6 +2040,11 @@ balanced-match@^1.0.0:
resolved "https://registry.yarnpkg.com/balanced-match/-/balanced-match-1.0.2.tgz#e83e3a7e3f300b34cb9d87f615fa0cbf357690ee"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
base32.js@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/base32.js/-/base32.js-0.0.1.tgz#d045736a57b1f6c139f0c7df42518a84e91bb2ba"
integrity sha512-EGHIRiegFa62/SsA1J+Xs2tIzludPdzM064N9wjbiEgHnGnJ1V0WEpA4pEwCYT5nDvZk3ubf0shqaCS7k6xeUQ==
base64-js@^1.3.1:
version "1.5.1"
resolved "https://registry.yarnpkg.com/base64-js/-/base64-js-1.5.1.tgz#1b1b440160a5bf7ad40b650f095963481903930a"
......@@ -6086,6 +6091,13 @@ spdx-license-ids@^3.0.0:
resolved "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.10.tgz"
integrity sha512-oie3/+gKf7QtpitB0LYLETe+k8SifzsX4KixvpOsbI6S0kRiRQ5MKOio8eMSAKQ17N06+wdEOXRiId+zOxo0hA==
speakeasy@^2.0.0:
version "2.0.0"
resolved "https://registry.yarnpkg.com/speakeasy/-/speakeasy-2.0.0.tgz#85c91a071b09a5cb8642590d983566165f57613a"
integrity sha512-lW2A2s5LKi8rwu77ewisuUOtlCydF/hmQSOJjpTqTj1gZLkNgTaYnyvfxy2WBr4T/h+9c4g8HIITfj83OkFQFw==
dependencies:
base32.js "0.0.1"
sprintf-js@~1.0.2:
version "1.0.3"
resolved "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz"
......
Supports Markdown
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment