From f8ec7c0bce86791fec2dafa60924e1c7532a5b0d Mon Sep 17 00:00:00 2001 From: Chakshu Gautam Date: Thu, 12 Dec 2024 22:03:58 +0530 Subject: [PATCH] Formatting issues + WA OTP Fix --- src/api/api.controller.ts | 128 ++++---- src/api/api.service.ts | 264 ++++++++++------- src/api/config.resolver.service.ts | 109 +++---- src/api/dto/send-otp.dto.ts | 7 +- .../gupshupWhatsapp.service.ts | 273 +++++++++--------- 5 files changed, 422 insertions(+), 359 deletions(-) diff --git a/src/api/api.controller.ts b/src/api/api.controller.ts index 2d0b6d5..150427c 100644 --- a/src/api/api.controller.ts +++ b/src/api/api.controller.ts @@ -22,7 +22,7 @@ import { UserRegistration, UsersResponse, ResponseCode, - ResponseStatus + ResponseStatus, } from './api.interface'; import { ApiService } from './api.service'; import { ConfigResolverService } from './config.resolver.service'; @@ -36,7 +36,7 @@ import * as Sentry from '@sentry/node'; import { LoginDto, LoginWithUniqueIdDto } from './dto/login.dto'; import { SendOtpDto } from './dto/send-otp.dto'; import { VerifyOtpDto } from './dto/verify-otp.dto'; -import { Throttle, SkipThrottle} from '@nestjs/throttler'; +import { Throttle, SkipThrottle } from '@nestjs/throttler'; import { ConfigService } from '@nestjs/config'; import { v4 as uuidv4 } from 'uuid'; import { VerifyJWTDto } from './dto/verify-jwt.dto'; @@ -61,7 +61,7 @@ export class ApiController { private readonly configResolverService: ConfigResolverService, private readonly gupshupWhatsappService: GupshupWhatsappService, private readonly telemetryService: TelemetryService, - @InjectRedis() private readonly redis: Redis + @InjectRedis() private readonly redis: Redis, ) {} @Get() @@ -79,9 +79,12 @@ export class ApiController { @Query() params: SendOtpDto, @Headers('x-application-id') applicationId?, ): Promise { - let startTime = Date.now(); + const startTime = Date.now(); - let status: any, isWhatsApp = false, countryCode, number; + let status: any, + isWhatsApp = false, + countryCode, + number; if (params.phone.includes('-')) { [countryCode, number] = params.phone.split('-'); @@ -111,35 +114,38 @@ export class ApiController { } } - // Check if phone number contains country code (e.g. 91-1234567890) - if (params.deliveryType=='WA') { + // Check if phone number contains country code (e.g. 91-1234567890) + if (params.deliveryType == 'WA') { isWhatsApp = true; - - status = await this.gupshupWhatsappService.sendWhatsappOTP({ - phone: number, - template: null, - type: null, - params: null - },countryCode); + + status = await this.gupshupWhatsappService.sendWhatsappOTP( + { + phone: number, + template: null, + type: null, + params: null, + }, + countryCode, + ); } else { status = await this.otpService.sendOTP(params.phone); } - if(this.configService.get('TELEMETRY_INTERNAL_BASE_URL')) { + if (this.configService.get('TELEMETRY_INTERNAL_BASE_URL')) { this.telemetryService.sendEvent( { botId: params.botId, orgId: params.orgId, - timeTaken: Date.now() - startTime, + timeTaken: Date.now() - startTime, createdAt: Math.floor(new Date().getTime() / 1000), phoneNumber: params.phone, - eventLog: `Response from OTP provider - ${status.providerSuccessResponse}` + eventLog: `Response from OTP provider - ${status.providerSuccessResponse}`, }, 'E117', 'Send OTP', 'sendOTP', - isWhatsApp ? 'Whatsapp' : 'PWA' - ) + isWhatsApp ? 'Whatsapp' : 'PWA', + ); } return { status }; } @@ -422,9 +428,9 @@ export class ApiController { async loginWithUniqueId( @Body() user: LoginWithUniqueIdDto, @Headers('authorization') authHeader, - @Headers('ADMIN-API-KEY') adminApiKey + @Headers('ADMIN-API-KEY') adminApiKey, ): Promise { - if(adminApiKey!=this.configService.get('ADMIN_API_KEY')){ + if (adminApiKey != this.configService.get('ADMIN_API_KEY')) { const response: SignupResponse = new SignupResponse().init(uuidv4()); response.responseCode = ResponseCode.FAILURE; response.params.err = 'UNAUTHORIZED'; @@ -434,20 +440,16 @@ export class ApiController { } return await this.apiService.loginWithUniqueId(user, authHeader); } - + @Post('jwt/verify') - @UsePipes(new ValidationPipe({transform: true})) - async jwtVerify( - @Body() body: VerifyJWTDto - ): Promise { + @UsePipes(new ValidationPipe({ transform: true })) + async jwtVerify(@Body() body: VerifyJWTDto): Promise { return await this.apiService.verifyJWT(body.token); } @Post('logout') - @UsePipes(new ValidationPipe({transform: true})) - async logout( - @Body() body: VerifyJWTDto - ): Promise { + @UsePipes(new ValidationPipe({ transform: true })) + async logout(@Body() body: VerifyJWTDto): Promise { return await this.apiService.logout(body.token); } @@ -469,14 +471,14 @@ export class ApiController { const url = new URL(`${fusionAuthBaseUrl}/api/user/search`); // Add query params to URL if (query) { - Object.keys(query).forEach(key => { + Object.keys(query).forEach((key) => { url.searchParams.append(key, query[key]); }); } // Add params to URL if (params) { - Object.keys(params).forEach(key => { + Object.keys(params).forEach((key) => { url.searchParams.append(key, params[key]); }); } @@ -512,27 +514,32 @@ export class ApiController { return JSON.parse(cachedData); } - const userData = await this.fetchUserDataFromService(id, authorization, appId); + const userData = await this.fetchUserDataFromService( + id, + authorization, + appId, + ); await this.redis.set(cacheKey, JSON.stringify(userData)); return userData; } - private async fetchUserDataFromService(id: string, authorization: string, appId: string) { + private async fetchUserDataFromService( + id: string, + authorization: string, + appId: string, + ) { try { const fusionAuthBaseUrl = this.configService.get('FUSIONAUTH_BASE_URL'); const url = new URL(`${fusionAuthBaseUrl}/api/user/${id}`); - const response = await fetch( - url, - { - method: "GET", - headers: { - Authorization: authorization, - 'Content-Type': 'application/json', - 'X-FusionAuth-Application-Id': appId, - }, + const response = await fetch(url, { + method: 'GET', + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + 'X-FusionAuth-Application-Id': appId, }, - ); + }); return response.json(); } catch (error) { throw new HttpException( @@ -542,22 +549,15 @@ export class ApiController { } } - private async searchUserData( - url: URL, - authorization: string, - appId: string, - ) { + private async searchUserData(url: URL, authorization: string, appId: string) { try { - const response = await fetch( - url, - { - headers: { - Authorization: authorization, - 'Content-Type': 'application/json', - 'X-FusionAuth-Application-Id': appId, - } + const response = await fetch(url, { + headers: { + Authorization: authorization, + 'Content-Type': 'application/json', + 'X-FusionAuth-Application-Id': appId, }, - ); + }); return response.json(); } catch (error) { throw new HttpException( @@ -578,17 +578,17 @@ export class ApiController { ): Promise { const fusionAuthBaseUrl = this.configService.get('FUSIONAUTH_BASE_URL'); const url = new URL(`${fusionAuthBaseUrl}${request.url}`); - + // Add query params to URL if (query) { - Object.keys(query).forEach(key => { + Object.keys(query).forEach((key) => { url.searchParams.append(key, query[key]); }); } // Add params to URL if (params) { - Object.keys(params).forEach(key => { + Object.keys(params).forEach((key) => { url.searchParams.append(key, params[key]); }); } @@ -597,10 +597,10 @@ export class ApiController { method: request.method, body: Object.keys(body).length ? JSON.stringify(body) : undefined, headers: { - 'Authorization': authHeader, + Authorization: authHeader, 'x-application-id': applicationId, - 'Content-Type': 'application/json' - } + 'Content-Type': 'application/json', + }, }); return await response.json(); diff --git a/src/api/api.service.ts b/src/api/api.service.ts index 67bc263..5cd5629 100644 --- a/src/api/api.service.ts +++ b/src/api/api.service.ts @@ -41,7 +41,7 @@ CryptoJS.lib.WordArray.words; @Injectable() export class ApiService { - private client: any + private client: any; private getKey: any; encodedBase64Key; parsedBase64Key; @@ -51,10 +51,10 @@ export class ApiService { private readonly otpService: OtpService, private readonly configResolverService: ConfigResolverService, @InjectRedis() private readonly redis: Redis, - private readonly gupshupWhatsappService: GupshupWhatsappService + private readonly gupshupWhatsappService: GupshupWhatsappService, ) { - this.client = jwksClient({ - jwksUri: this.configService.get("JWKS_URI"), + this.client = jwksClient({ + jwksUri: this.configService.get('JWKS_URI'), requestHeaders: {}, // Optional timeout: 30000, // Defaults to 30s }); @@ -123,14 +123,16 @@ export class ApiService { user: fusionAuthUser, }, }; - if( + if ( this.configService.get('USE_FLAGSMITH') === 'true' && this.configService.get('FLAGSMITH_ENVIRONMENT_KEY') - ){ - let flagsmith = new Flagsmith({ - environmentKey: this.configService.get("FLAGSMITH_ENVIRONMENT_KEY") + ) { + const flagsmith = new Flagsmith({ + environmentKey: this.configService.get('FLAGSMITH_ENVIRONMENT_KEY'), }); - await flagsmith.getIdentityFlags(fusionAuthUser.user.username, ['role']); + await flagsmith.getIdentityFlags(fusionAuthUser.user.username, [ + 'role', + ]); } return response; }) @@ -145,7 +147,8 @@ export class ApiService { } else if (errorResponse.statusCode === 409) { response.responseCode = ResponseCode.FAILURE; response.params.err = 'ACCOUNT_LOCKED'; - response.params.errMsg = 'Multiple failed login attempts. Please retry again later.'; + response.params.errMsg = + 'Multiple failed login attempts. Please retry again later.'; response.params.status = ResponseStatus.failure; } else { response.responseCode = ResponseCode.FAILURE; @@ -304,7 +307,8 @@ export class ApiService { userId: string, data: User, applicationId: string, - authHeader?: string,): Promise { + authHeader?: string, + ): Promise { const registrations: Array = data?.registrations ? data.registrations : []; @@ -324,7 +328,12 @@ export class ApiService { // if there are registrations Array, we'll update the registrations too for (const registration of registrations) { console.log(`Updating registration: ${JSON.stringify(registration)}`); - await this.updateUserRegistration(applicationId, authHeader, userId, registration); // calling patch registration API + await this.updateUserRegistration( + applicationId, + authHeader, + userId, + registration, + ); // calling patch registration API } const response: SignupResponse = new SignupResponse().init(uuidv4()); @@ -551,7 +560,10 @@ export class ApiService { return response; } - async loginWithOtp(loginDto: LoginDto, authHeader: null | string): Promise { + async loginWithOtp( + loginDto: LoginDto, + authHeader: null | string, + ): Promise { /* Execution flow 1. Check if ALLOW_DEFAULT_OTP is set to true. 2. If true check if user number is listed in DEFAULT_OTP_USERS, if yes send sucess if OTP matches. @@ -562,45 +574,64 @@ export class ApiService { 3.2. If new user, register to this application. 4. Send login response with the token */ - let otp = loginDto.password; + const otp = loginDto.password; let phone = loginDto.loginId; let countryCode, number; if (phone.includes('-')) { [countryCode, number] = phone.split('-'); phone = number; - } + } const salt = this.configResolverService.getSalt(loginDto.applicationId); let verifyOTPResult; - if( - this.configService.get("ALLOW_DEFAULT_OTP") === 'true' && - this.configService.get("DEFAULT_OTP_USERS") - ){ - if(JSON.parse(this.configService.get("DEFAULT_OTP_USERS")).indexOf(loginDto.loginId)!=-1){ - if(loginDto.password == this.configService.get("DEFAULT_OTP")) - verifyOTPResult = {status: SMSResponseStatus.success} - else - verifyOTPResult = {status: SMSResponseStatus.failure} - } else if (loginDto.deliveryType=='WA') { - loginDto.loginId = phone; - const status: any = await this.gupshupWhatsappService.verifyWhatsappOTP(loginDto.loginId, loginDto.password); - if(status.status == 'success') { - verifyOTPResult = {status: SMSResponseStatus.success} + if ( + this.configService.get('ALLOW_DEFAULT_OTP') === 'true' && + this.configService.get('DEFAULT_OTP_USERS') + ) { + if ( + JSON.parse(this.configService.get('DEFAULT_OTP_USERS')).indexOf( + loginDto.loginId, + ) != -1 + ) { + if (loginDto.password == this.configService.get('DEFAULT_OTP')) + verifyOTPResult = { status: SMSResponseStatus.success }; + else verifyOTPResult = { status: SMSResponseStatus.failure }; + } else if (loginDto.deliveryType == 'WA') { + loginDto.loginId = loginDto.loginId; + const status: any = await this.gupshupWhatsappService.verifyWhatsappOTP( + loginDto.loginId, + loginDto.password, + ); + if (status.status == 'success') { + verifyOTPResult = { status: SMSResponseStatus.success }; } else { - verifyOTPResult = {status: SMSResponseStatus.failure} + verifyOTPResult = { status: SMSResponseStatus.failure }; } - } else { + } else { + verifyOTPResult = await this.otpService.verifyOTP({ + phone: loginDto.loginId, + otp: loginDto.password, // existing OTP + }); + } + } else { + if (loginDto.deliveryType == 'WA') { + loginDto.loginId = loginDto.loginId; + const status: any = await this.gupshupWhatsappService.verifyWhatsappOTP( + loginDto.loginId, + loginDto.password, + ); + if (status.status == 'success') { + verifyOTPResult = { status: SMSResponseStatus.success }; + } else { + verifyOTPResult = { status: SMSResponseStatus.failure }; + } + } else { verifyOTPResult = await this.otpService.verifyOTP({ phone: loginDto.loginId, otp: loginDto.password, // existing OTP }); } - } else { - verifyOTPResult = await this.otpService.verifyOTP({ - phone: loginDto.loginId, - otp: loginDto.password, // existing OTP - }); } - loginDto.password = salt + loginDto.password; // mix OTP with salt + loginDto.password = salt + loginDto.password; // mix OTP with salt if (verifyOTPResult.status === SMSResponseStatus.success) { let response; @@ -615,7 +646,8 @@ export class ApiService { authHeader, ); if (statusFA === FAStatus.USER_EXISTS) { - let registrationId = null, registeredRoles = []; + let registrationId = null, + registeredRoles = []; if (user.registrations) { user.registrations.map((item) => { if (item.applicationId == loginDto.applicationId) { @@ -641,8 +673,8 @@ export class ApiService { loginId: loginDto.loginId, fingerprint: loginDto?.fingerprint, timestamp: loginDto?.timestamp, - otp - } + otp, + }, }, loginDto.applicationId, authHeader, @@ -652,7 +684,7 @@ export class ApiService { // create a new user const createUserPayload: UserRegistration = { user: { - timezone: "Asia/Kolkata", + timezone: 'Asia/Kolkata', username: loginDto.loginId, mobilePhone: loginDto.loginId, password: loginDto.password, @@ -660,17 +692,15 @@ export class ApiService { loginId: loginDto.loginId, fingerprint: loginDto?.fingerprint, timestamp: loginDto?.timestamp, - otp - } + otp, + }, }, registration: { applicationId: loginDto.applicationId, - preferredLanguages: [ - "en" - ], - roles: loginDto.roles ?? [], // pass from request body if present, else empty list - } - } + preferredLanguages: ['en'], + roles: loginDto.roles ?? [], // pass from request body if present, else empty list + }, + }; const { userId, user, err }: { userId: UUID; user: User; err: Error } = await this.fusionAuthService.createAndRegisterUser( createUserPayload, @@ -682,14 +712,19 @@ export class ApiService { } response = await this.login(loginDto, authHeader); } - let existingJWTS:any = await this.redis.get(response?.result?.data?.user?.user?.id); - if(existingJWTS) { + let existingJWTS: any = await this.redis.get( + response?.result?.data?.user?.user?.id, + ); + if (existingJWTS) { existingJWTS = JSON.parse(existingJWTS); } else { - existingJWTS = [] + existingJWTS = []; } existingJWTS.push(response?.result?.data?.user?.token); - await this.redis.set(response?.result?.data?.user?.user?.id, JSON.stringify(existingJWTS)); + await this.redis.set( + response?.result?.data?.user?.user?.id, + JSON.stringify(existingJWTS), + ); return response; } else { const response: SignupResponse = new SignupResponse().init(uuidv4()); @@ -701,7 +736,10 @@ export class ApiService { } } - async loginWithUniqueId(loginDto: LoginWithUniqueIdDto, authHeader: null | string): Promise { + async loginWithUniqueId( + loginDto: LoginWithUniqueIdDto, + authHeader: null | string, + ): Promise { /* Execution flow 1. Check if user exists for the given applicationId and loginId. 3.1. If existing user, login user with default password. @@ -709,35 +747,31 @@ export class ApiService { 4. Send login response with the token */ const salt = this.configResolverService.getSalt(loginDto.applicationId); - let password = salt + this.configService.get("DEFAULT_USER_PASSWORD"); // mix OTP with salt - console.log(password) + const password = salt + this.configService.get('DEFAULT_USER_PASSWORD'); // mix OTP with salt + console.log(password); - const { - statusFA - }: { statusFA: FAStatus} = + const { statusFA }: { statusFA: FAStatus } = await this.fusionAuthService.getUser( loginDto.loginId, loginDto.applicationId, authHeader, ); if (statusFA === FAStatus.USER_EXISTS) { - return this.login({...loginDto,password}, authHeader); + return this.login({ ...loginDto, password }, authHeader); } else { // create a new user const createUserPayload: UserRegistration = { user: { - timezone: "Asia/Kolkata", + timezone: 'Asia/Kolkata', username: loginDto.loginId, - password: password + password: password, }, registration: { applicationId: loginDto.applicationId, - preferredLanguages: [ - "en" - ], + preferredLanguages: ['en'], roles: [], - } - } + }, + }; const { userId, user, err }: { userId: UUID; user: User; err: Error } = await this.fusionAuthService.createAndRegisterUser( createUserPayload, @@ -747,7 +781,7 @@ export class ApiService { if (userId == null || user == null) { throw new HttpException(err, HttpStatus.BAD_REQUEST); } - return this.login({...loginDto,password}, authHeader); + return this.login({ ...loginDto, password }, authHeader); } } @@ -762,7 +796,12 @@ export class ApiService { registration, err, }: { _userId: UUID; registration: FusionAuthUserRegistration; err: Error } = - await this.fusionAuthService.updateUserRegistration(applicationId, authHeader, userId, data); + await this.fusionAuthService.updateUserRegistration( + applicationId, + authHeader, + userId, + data, + ); if (_userId == null || registration == null) { throw new HttpException(err, HttpStatus.BAD_REQUEST); @@ -772,71 +811,76 @@ export class ApiService { async verifyFusionAuthJWT(token: string): Promise { return new Promise((resolve, reject) => { - jwt.verify(token, this.getKey, async (err, decoded) => { - if (err) { - console.error('APP JWT verification error:', err); - resolve({ - isValidFusionAuthToken: false, - claims: null - }) - } else { - resolve({ - isValidFusionAuthToken: true, - claims: decoded - }) - } + jwt.verify(token, this.getKey, async (err, decoded) => { + if (err) { + console.error('APP JWT verification error:', err); + resolve({ + isValidFusionAuthToken: false, + claims: null, + }); + } else { + resolve({ + isValidFusionAuthToken: true, + claims: decoded, + }); + } + }); }); - }); } - async verifyJWT(token:string): Promise { - const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token); - let existingUserJWTS:any="[]" + async verifyJWT(token: string): Promise { + const { isValidFusionAuthToken, claims } = await this.verifyFusionAuthJWT( + token, + ); + let existingUserJWTS: any = '[]'; - if(claims?.sub) { + if (claims?.sub) { existingUserJWTS = JSON.parse(await this.redis.get(claims.sub)); } - - if(!isValidFusionAuthToken){ - if(existingUserJWTS.indexOf(token)!=-1){ + + if (!isValidFusionAuthToken) { + if (existingUserJWTS.indexOf(token) != -1) { existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1); await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS)); } return { - "isValid": false, - "message": "Invalid/Expired token." - } + isValid: false, + message: 'Invalid/Expired token.', + }; } - if(existingUserJWTS.indexOf(token)==-1){ + if (existingUserJWTS.indexOf(token) == -1) { return { - "isValid": false, - "message": "Token is not authorized." - } + isValid: false, + message: 'Token is not authorized.', + }; } return { - "isValid": true, - "message": "Token is valid." - } + isValid: true, + message: 'Token is valid.', + }; } - async logout(token:string): Promise { - const { isValidFusionAuthToken, claims} = await this.verifyFusionAuthJWT(token); - if(isValidFusionAuthToken){ - let existingUserJWTS:any = JSON.parse(await this.redis.get(claims.sub)); - if(existingUserJWTS.indexOf(token)!=-1){ + async logout(token: string): Promise { + const { isValidFusionAuthToken, claims } = await this.verifyFusionAuthJWT( + token, + ); + if (isValidFusionAuthToken) { + const existingUserJWTS: any = JSON.parse( + await this.redis.get(claims.sub), + ); + if (existingUserJWTS.indexOf(token) != -1) { existingUserJWTS.splice(existingUserJWTS.indexOf(token), 1); await this.redis.set(claims.sub, JSON.stringify(existingUserJWTS)); } return { - "message": "Logout successful. Token invalidated." - } + message: 'Logout successful. Token invalidated.', + }; } else { return { - "message": "Invalid or expired token." - } + message: 'Invalid or expired token.', + }; } } - } diff --git a/src/api/config.resolver.service.ts b/src/api/config.resolver.service.ts index ab4dffa..99ef80d 100644 --- a/src/api/config.resolver.service.ts +++ b/src/api/config.resolver.service.ts @@ -1,66 +1,67 @@ -import { Injectable } from "@nestjs/common"; -import { ConfigService } from "@nestjs/config"; -import { ApiConfig } from "./api.interface"; +import { Injectable } from '@nestjs/common'; +import { ConfigService } from '@nestjs/config'; +import { ApiConfig } from './api.interface'; @Injectable() export class ConfigResolverService { - constructor(private configService: ConfigService) { - } + constructor(private configService: ConfigService) {} - transform(applicationId: string): string { - /** - * we'll replace "-" with "_" because docker-compose cannot load hyphens & we expect applicationId with underscore separator. - * Also, we'll prefix as the uuid may begin with integer & variables can't begin with numbers. - */ - return 'APP_' + applicationId.split("-").join("_"); - } + transform(applicationId: string): string { + /** + * we'll replace "-" with "_" because docker-compose cannot load hyphens & we expect applicationId with underscore separator. + * Also, we'll prefix as the uuid may begin with integer & variables can't begin with numbers. + */ + return 'APP_' + applicationId.split('-').join('_'); + } - getConfigByApplicationId(applicationId: string): ApiConfig { - applicationId = this.transform(applicationId); - return this.configService.get(applicationId); - } + getConfigByApplicationId(applicationId: string): ApiConfig { + applicationId = this.transform(applicationId); + return this.configService.get(applicationId); + } - getHost(applicationId: string): string { - applicationId = this.transform(applicationId); - const config = this.configService.get(applicationId); - return JSON.parse(config).host; - } + getHost(applicationId: string): string { + applicationId = this.transform(applicationId); + const config = this.configService.get(applicationId); + return JSON.parse(config).host; + } - getApiKey(applicationId: string): string { - applicationId = this.transform(applicationId); - const config = this.configService.get(applicationId); - return JSON.parse(config).apiKey || null; - } + getApiKey(applicationId: string): string { + applicationId = this.transform(applicationId); + const config = this.configService.get(applicationId); + return JSON.parse(config).apiKey || null; + } - getEncryptionStatus(applicationId: string): boolean { - applicationId = this.transform(applicationId); - const config = this.configService.get(applicationId); - return JSON.parse(config).encryption.enabled || false; - } + getEncryptionStatus(applicationId: string): boolean { + applicationId = this.transform(applicationId); + const config = this.configService.get(applicationId); + return JSON.parse(config).encryption.enabled || false; + } - getEncryptionKey(applicationId: string): string{ - if (this.getEncryptionStatus(applicationId)) { - applicationId = this.transform(applicationId); - const config = this.configService.get(applicationId); - return JSON.parse(config).encryption.key || undefined; - } - return undefined; + getEncryptionKey(applicationId: string): string { + if (this.getEncryptionStatus(applicationId)) { + applicationId = this.transform(applicationId); + const config = this.configService.get(applicationId); + return JSON.parse(config).encryption.key || undefined; } + return undefined; + } - getHasura(applicationId: string): { - graphql_url: string, - admin_secret: string, - enabled: boolean, - mutations: object - } | undefined { - applicationId = this.transform(applicationId); - const config = this.configService.get(applicationId); - return config ? (JSON.parse(config)?.hasura || undefined) : undefined; - } + getHasura(applicationId: string): + | { + graphql_url: string; + admin_secret: string; + enabled: boolean; + mutations: object; + } + | undefined { + applicationId = this.transform(applicationId); + const config = this.configService.get(applicationId); + return config ? JSON.parse(config)?.hasura || undefined : undefined; + } - getSalt(applicationId: string): string { - applicationId = this.transform(applicationId); - const config = this.configService.get(applicationId); - return JSON.parse(config).salt || null; - } -} \ No newline at end of file + getSalt(applicationId: string): string { + applicationId = this.transform(applicationId); + const config = this.configService.get(applicationId); + return JSON.parse(config).salt || null; + } +} diff --git a/src/api/dto/send-otp.dto.ts b/src/api/dto/send-otp.dto.ts index 791448a..0caad57 100644 --- a/src/api/dto/send-otp.dto.ts +++ b/src/api/dto/send-otp.dto.ts @@ -1,11 +1,14 @@ import { - IsNotEmpty, IsOptional, IsPhoneNumber, IsString, + IsNotEmpty, + IsOptional, + IsPhoneNumber, + IsString, } from 'class-validator'; export class SendOtpDto { @IsString() @IsNotEmpty() - @IsPhoneNumber('IN') + // @IsPhoneNumber('IN') phone: string; @IsString() diff --git a/src/api/sms/gupshupWhatsapp/gupshupWhatsapp.service.ts b/src/api/sms/gupshupWhatsapp/gupshupWhatsapp.service.ts index 36e7470..2f45c81 100644 --- a/src/api/sms/gupshupWhatsapp/gupshupWhatsapp.service.ts +++ b/src/api/sms/gupshupWhatsapp/gupshupWhatsapp.service.ts @@ -1,139 +1,154 @@ -import { SMSData, SMSProvider, SMSResponse, SMSResponseStatus } from "../sms.interface"; +import { + SMSData, + SMSProvider, + SMSResponse, + SMSResponseStatus, +} from '../sms.interface'; import { InjectRedis } from '@nestjs-modules/ioredis'; import Redis from 'ioredis'; -import { Injectable } from "@nestjs/common"; +import { Injectable } from '@nestjs/common'; import axios from 'axios'; @Injectable() export class GupshupWhatsappService { - - constructor( - @InjectRedis() private readonly redis: Redis - ) { + constructor(@InjectRedis() private readonly redis: Redis) {} + + async sendWhatsappOTP( + smsData: SMSData, + countryCode: string, + ): Promise { + const status: SMSResponse = { + providerResponseCode: null, + status: SMSResponseStatus.failure, + messageID: null, + error: null, + providerSuccessResponse: null, + phone: smsData.phone, + networkResponseCode: null, + provider: SMSProvider.gupshup, + }; + + // Generate 4 digit OTP + const otp = Math.floor(1000 + Math.random() * 9000); + try { + // First opt-in the user + const optInParams = new URLSearchParams(); + optInParams.append('method', 'OPT_IN'); + optInParams.append('format', 'text'); + optInParams.append('userid', process.env.GUPSHUP_WHATSAPP_USERID); + optInParams.append('password', process.env.GUPSHUP_WHATSAPP_PASSWORD); + optInParams.append('phone_number', `${countryCode}${smsData.phone}`); + optInParams.append('v', '1.1'); + optInParams.append('auth_scheme', 'plain'); + optInParams.append('channel', 'WHATSAPP'); + + const optinURL = + process.env.GUPSHUP_WHATSAPP_BASEURL + '?' + optInParams.toString(); + + await axios.get(optinURL, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + // Then send OTP message + const otpMessage = process.env.GUPSHUP_WHATSAPP_OTP_TEMPLATE.replace( + '%otp%', + `${otp}`, + ); + + const sendOtpParams = new URLSearchParams(); + sendOtpParams.append('method', 'SENDMESSAGE'); + sendOtpParams.append('userid', process.env.GUPSHUP_WHATSAPP_USERID); + sendOtpParams.append('password', process.env.GUPSHUP_WHATSAPP_PASSWORD); + sendOtpParams.append('send_to', `${countryCode}${smsData.phone}`); + sendOtpParams.append('v', '1.1'); + sendOtpParams.append('format', 'json'); + sendOtpParams.append('msg_type', 'TEXT'); + sendOtpParams.append('msg', otpMessage); + sendOtpParams.append('isTemplate', 'true'); + sendOtpParams.append( + 'footer', + process.env.GUPSHUP_WHATSAPP_OTP_TEMPLATE_FOOTER, + ); + + const sendOtpURL = + process.env.GUPSHUP_WHATSAPP_BASEURL + '?' + sendOtpParams.toString(); + + const response = await axios.get(sendOtpURL, { + headers: { + 'Content-Type': 'application/json', + }, + }); + + if (response.status === 200) { + // Store OTP in Redis with 30 minute expiry + await this.redis.set( + `whatsapp_otp:+${countryCode.trim()}-${smsData.phone}`, + otp.toString(), + 'EX', + 1800, + ); + + status.providerSuccessResponse = JSON.stringify(response.data); + status.status = SMSResponseStatus.success; + } + + return status; + } catch (error) { + status.error = { + errorCode: error.code || 'WHATSAPP_ERROR', + errorText: error.message, + }; + return status; } - - async sendWhatsappOTP(smsData: SMSData, countryCode: string): Promise { - const status: SMSResponse = { - providerResponseCode: null, - status: SMSResponseStatus.failure, - messageID: null, - error: null, - providerSuccessResponse: null, - phone: smsData.phone, - networkResponseCode: null, - provider: SMSProvider.gupshup + } + + async verifyWhatsappOTP(phone: string, otp: string): Promise { + const status: SMSResponse = { + providerResponseCode: null, + status: SMSResponseStatus.failure, + messageID: null, + error: null, + providerSuccessResponse: null, + phone: phone, + networkResponseCode: null, + provider: SMSProvider.gupshup, + }; + + try { + // Get stored OTP from Redis + const storedOTP = await this.redis.get(`whatsapp_otp:${phone}`); + + if (!storedOTP) { + status.error = { + errorCode: 'OTP_EXPIRED', + errorText: 'OTP has expired or does not exist', }; - - // Generate 4 digit OTP - const otp = Math.floor(1000 + Math.random() * 9000); - try { - // First opt-in the user - const optInParams = new URLSearchParams(); - optInParams.append("method", "OPT_IN"); - optInParams.append("format", "text"); - optInParams.append("userid", process.env.GUPSHUP_WHATSAPP_USERID); - optInParams.append("password", process.env.GUPSHUP_WHATSAPP_PASSWORD); - optInParams.append("phone_number", `${countryCode}${smsData.phone}`); - optInParams.append("v", "1.1"); - optInParams.append("auth_scheme", "plain"); - optInParams.append("channel", "WHATSAPP"); - - let optinURL = process.env.GUPSHUP_WHATSAPP_BASEURL + '?' + optInParams.toString(); - - await axios.get(optinURL, { - headers: { - 'Content-Type': 'application/json' - } - }); - - // Then send OTP message - const otpMessage = process.env.GUPSHUP_WHATSAPP_OTP_TEMPLATE.replace('%otp%',`${otp}`); - - const sendOtpParams = new URLSearchParams(); - sendOtpParams.append("method", "SENDMESSAGE"); - sendOtpParams.append("userid", process.env.GUPSHUP_WHATSAPP_USERID); - sendOtpParams.append("password", process.env.GUPSHUP_WHATSAPP_PASSWORD); - sendOtpParams.append("send_to", `${countryCode}${smsData.phone}`); - sendOtpParams.append("v", "1.1"); - sendOtpParams.append("format", "json"); - sendOtpParams.append("msg_type", "TEXT"); - sendOtpParams.append("msg", otpMessage); - sendOtpParams.append("isTemplate", "true"); - sendOtpParams.append("footer", process.env.GUPSHUP_WHATSAPP_OTP_TEMPLATE_FOOTER); - - let sendOtpURL = process.env.GUPSHUP_WHATSAPP_BASEURL + '?' + sendOtpParams.toString(); - - const response = await axios.get(sendOtpURL, { - headers: { - 'Content-Type': 'application/json' - } - }); - - if (response.status === 200) { - // Store OTP in Redis with 30 minute expiry - await this.redis.set(`whatsapp_otp:${smsData.phone}`, otp.toString(), 'EX', 1800); - - status.providerSuccessResponse = JSON.stringify(response.data); - status.status = SMSResponseStatus.success; - } - - return status; - - } catch (error) { - status.error = { - errorCode: error.code || 'WHATSAPP_ERROR', - errorText: error.message - }; - return status; - } - } - - async verifyWhatsappOTP(phone: string, otp: string): Promise { - const status: SMSResponse = { - providerResponseCode: null, - status: SMSResponseStatus.failure, - messageID: null, - error: null, - providerSuccessResponse: null, - phone: phone, - networkResponseCode: null, - provider: SMSProvider.gupshup + return status; + } + + if (storedOTP === otp) { + // OTP matches + status.status = SMSResponseStatus.success; + status.providerSuccessResponse = 'OTP verified successfully'; + + // Delete the OTP from Redis after successful verification + await this.redis.del(`whatsapp_otp:${phone}`); + } else { + status.error = { + errorCode: 'INVALID_OTP', + errorText: 'Invalid OTP provided', }; - - try { - // Get stored OTP from Redis - const storedOTP = await this.redis.get(`whatsapp_otp:${phone}`); - - if (!storedOTP) { - status.error = { - errorCode: 'OTP_EXPIRED', - errorText: 'OTP has expired or does not exist' - }; - return status; - } - - if (storedOTP === otp) { - // OTP matches - status.status = SMSResponseStatus.success; - status.providerSuccessResponse = 'OTP verified successfully'; - - // Delete the OTP from Redis after successful verification - await this.redis.del(`whatsapp_otp:${phone}`); - } else { - status.error = { - errorCode: 'INVALID_OTP', - errorText: 'Invalid OTP provided' - }; - } - - return status; - - } catch (error) { - status.error = { - errorCode: error.code || 'VERIFICATION_ERROR', - errorText: error.message - }; - return status; - } + } + + return status; + } catch (error) { + status.error = { + errorCode: error.code || 'VERIFICATION_ERROR', + errorText: error.message, + }; + return status; } -} \ No newline at end of file + } +}