This guide provides a step-by-step walkthrough for adding SMS-based One-Time Password (OTP) verification, a common form of Two-Factor Authentication (2FA), to your Node.js Express application using the Infobip API. We'll cover everything from initial setup to production considerations.
Enhancing application security is crucial, and OTP via SMS provides a robust layer by verifying user possession of a registered phone number. This guide aims to equip you with the knowledge to implement this feature effectively.
Project Overview and Goals
What We'll Build:
We will build a simple Node.js Express application with two core API endpoints:
/request-otp
: Accepts a user's phone number, uses the Infobip API to generate and send an OTP code via SMS to that number, and stores necessary information for verification./verify-otp
: Accepts the user's phone number and the OTP code they received, then uses the Infobip API to verify if the code is correct and valid.
Problem Solved:
This implementation adds a layer of security to user actions like registration, login, or sensitive operations by verifying that the user currently possesses the phone number associated with their account. It helps prevent unauthorized access even if account credentials (like passwords) are compromised.
Technologies Used:
- Node.js: The JavaScript runtime environment.
- Express.js: A minimal and flexible Node.js web application framework for building the API.
- Infobip 2FA API: The third-party service used to handle OTP generation, SMS delivery, and verification.
axios
: A promise-based HTTP client for making requests to the Infobip API.dotenv
: A module to load environment variables from a.env
file.- (Optional)
express-validator
: For robust request validation. - (Optional)
express-rate-limit
: To protect endpoints from brute-force attacks.
System Architecture:
+-------------+ +-----------------------+ +---------------+ +-------------+
| |--(1)->| Node.js/Express |--(2)->| |--(3)->| |
| End User | | App Server | | Infobip API | | User's Phone|
| (Browser/App)|<- F -| |<- G -| |<- E -| (SMS) |
| |<-(6)--| (API: /verify-otp) |<-(7)--| | | |
| | | (API: /request-otp) |--(4)->| | | |
+-------------+ +-----------------------+ +---------------+ +-------------+
Flow:
1. User initiates OTP request (e.g., provides phone number) to the App Server (/request-otp).
2. App Server sends OTP request details (App ID, Message ID, Phone Number) to Infobip API.
3. Infobip generates OTP, sends SMS to User's Phone.
4. Infobip returns a `pinId` (PIN identifier) to the App Server. (App Server stores this temporarily associated with the phone number).
5. User receives SMS, enters the OTP code into their Browser/App.
6. User submits the OTP code and phone number to the App Server (/verify-otp). App Server includes the stored `pinId`.
7. App Server sends the `pinId` and submitted OTP code to Infobip for verification.
G. Infobip returns verification status (success/failure) to App Server.
F. App Server processes the result and responds to the End User.
(Note: The visual placement of arrows F and G in the diagram might seem slightly ambiguous relative to steps 5/6/7, but the text description G->F reflects the logical sequence of responses after step 7).
Prerequisites:
- Node.js and npm (or yarn) installed.
- An active Infobip account (Sign up for free).
- A registered phone number (for testing during development, preferably the one linked to your Infobip trial account).
- Basic understanding of Node.js, Express, APIs, and asynchronous JavaScript.
Final Outcome:
A functional Express API capable of sending OTPs via Infobip SMS and verifying them, ready to be integrated into a larger application for enhanced security.
1. Setting up the Project
Let's initialize our Node.js project and install the necessary dependencies.
-
Create Project Directory: Open your terminal and create a new directory for the project, then navigate into it.
mkdir infobip-otp-guide cd infobip-otp-guide
-
Initialize npm:
npm init -y
This creates a
package.json
file. -
Install Dependencies: We need Express for the server,
axios
to call the Infobip API, anddotenv
to manage environment variables.npm install express axios dotenv
-
Install Development Dependencies (Optional but Recommended):
nodemon
helps during development by automatically restarting the server on file changes.npm install --save-dev nodemon
-
Configure
nodemon
(Optional): Add adev
script to yourpackage.json
scripts
section:// package.json { // ... other fields ""scripts"": { ""start"": ""node server.js"", ""dev"": ""nodemon server.js"", ""test"": ""echo \""Error: no test specified\"" && exit 1"" }, // ... other fields }
-
Create Project Structure: Organize the project for clarity. Create the following files and folders:
infobip-otp-guide/ ├── node_modules/ ├── .env ├── .gitignore ├── package.json ├── package-lock.json ├── server.js └── services/ └── infobip.js
-
Create
.gitignore
: Addnode_modules
and.env
to prevent committing them to version control.# .gitignore node_modules .env
-
Create
server.js
(Initial Setup):// server.js require('dotenv').config(); // Load environment variables early const express = require('express'); const infobipService = require('./services/infobip'); // We will create this soon const app = express(); const PORT = process.env.PORT || 3000; // --- Middleware --- app.use(express.json()); // Parse JSON request bodies // --- In-Memory Storage (DEMO ONLY - NOT FOR PRODUCTION) --- // !! CRITICAL !! In production, use a secure, persistent store like Redis or a database // table with TTL to associate pinId with the user/phone number/session. // In-memory storage is NOT scalable, NOT persistent, and prone to memory leaks if // the cleanup mechanism fails or if many requests are made without verification. const otpStore = {}; // { phoneNumber: pinId } // --- Routes --- app.post('/request-otp', async (req, res) => { const { phoneNumber } = req.body; if (!phoneNumber) { return res.status(400).json({ success: false, error: 'Phone number is required' }); } try { console.log(`Requesting OTP for ${phoneNumber}`); const pinId = await infobipService.sendOtp(phoneNumber); console.log(`OTP request successful for ${phoneNumber}, pinId: ${pinId}`); // Store the pinId temporarily, associating it with the phone number // AGAIN: Replace this with a proper storage solution in production. otpStore[phoneNumber] = pinId; // Basic cleanup for demo purposes. A robust system needs better handling. // This timeout might not run if the server crashes, potentially leaking memory. setTimeout(() => { // Only delete if the pinId hasn't been overwritten by a newer request for the same number if (otpStore[phoneNumber] === pinId) { delete otpStore[phoneNumber]; console.log(`Cleaned up expired/unused pinId for ${phoneNumber}`); } }, 10 * 60 * 1000); // 10 minutes (adjust based on pinTimeToLive) res.status(200).json({ success: true, message: 'OTP sent successfully.' }); } catch (error) { console.error('Error sending OTP:', error.response?.data || error.message); // Provide a generic error message to the client res.status(500).json({ success: false, error: 'Failed to send OTP. Please try again later.', // Optionally include details in logs or specific non-sensitive error codes errorCode: error.response?.data?.requestError?.serviceException?.messageId || 'UNKNOWN_ERROR' }); } }); app.post('/verify-otp', async (req, res) => { const { phoneNumber, otp } = req.body; if (!phoneNumber || !otp) { return res.status(400).json({ success: false, error: 'Phone number and OTP are required' }); } // Retrieve the pinId associated with this phone number (from demo store) const pinId = otpStore[phoneNumber]; if (!pinId) { console.warn(`No active OTP request found for ${phoneNumber}. It might have expired or never existed.`); return res.status(400).json({ success: false, error: 'No active OTP request found or it has expired. Please request a new OTP.' }); } try { console.log(`Verifying OTP ${otp} for ${phoneNumber} with pinId ${pinId}`); const isVerified = await infobipService.verifyOtp(pinId, otp); if (isVerified) { console.log(`OTP verification successful for ${phoneNumber}`); // Verification successful - remove the used pinId from store immediately delete otpStore[phoneNumber]; res.status(200).json({ success: true, message: 'OTP verified successfully.' }); // TODO: Update user status in your database (e.g., mark phone as verified) } else { console.warn(`OTP verification failed for ${phoneNumber}. Incorrect PIN.`); // Verification failed (likely incorrect PIN) // NOTE: Infobip handles attempt limits based on Application settings res.status(400).json({ success: false, error: 'Invalid OTP code.' }); // Consider if you want to clear pinId from store on failure based on app logic } } catch (error) { console.error('Error verifying OTP:', error.response?.data || error.message); // Check for specific Infobip errors if needed const errorCode = error.response?.data?.requestError?.serviceException?.messageId; let errorMessage = 'Failed to verify OTP. Please try again later.'; if (errorCode === 'TOO_MANY_REQUESTS') { errorMessage = 'Too many verification attempts. Please try again later.'; } else if (errorCode === 'PIN_EXPIRED') { errorMessage = 'OTP code has expired. Please request a new one.'; // Clean up expired pin from store if it matches the one we tried to verify if (otpStore[phoneNumber] === pinId) delete otpStore[phoneNumber]; } else if (errorCode === 'PIN_NOT_FOUND') { errorMessage = 'Invalid request. Please request a new OTP.'; // Clean up potentially invalid pin from store if it matches if (otpStore[phoneNumber] === pinId) delete otpStore[phoneNumber]; } res.status(500).json({ success: false, error: errorMessage, errorCode: errorCode || 'UNKNOWN_ERROR' }); } }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); infobipService.initialize() .then(() => console.log('Infobip service initialized.')) .catch(err => console.error('Failed to initialize Infobip service:', err)); });
-
Create
services/infobip.js
: This file will contain the logic for interacting with the Infobip API.// services/infobip.js const axios = require('axios'); // --- Configuration --- // These should come from environment variables const INFOBIP_API_KEY = process.env.INFOBIP_API_KEY; const INFOBIP_BASE_URL = process.env.INFOBIP_BASE_URL; // Example values - Consider making these configurable via .env or other means const APP_NAME = process.env.INFOBIP_APP_NAME || 'MyNodejsApp 2FA'; // Example name const SENDER_ID = process.env.INFOBIP_SENDER_ID || 'InfoSMS'; // Example sender - may need registration const MESSAGE_TEMPLATE_TEXT = process.env.INFOBIP_MESSAGE_TEMPLATE || 'Your verification PIN is {{pin}}. It expires in 5 minutes.'; // Example template text // --- State (to store IDs retrieved from Infobip) --- // Populated from .env or dynamically created on first run. let applicationId = process.env.INFOBIP_APPLICATION_ID; let messageId = process.env.INFOBIP_MESSAGE_ID; // --- Axios Instance --- let infobipAxios; // Define here, initialize after checking config // --- Helper Functions --- /** * Creates a 2FA Application in Infobip. * Lacks idempotency check: Assumes it needs creation if ID is missing. * In production, consider listing apps by name first if API allows. * @returns {Promise<string>} The Application ID. */ async function createApplication() { try { console.log(`Attempting to create Infobip 2FA Application: ${APP_NAME}`); // Note: This doesn't check if an app with this name already exists. // Running this multiple times without saving the ID will create duplicates. const response = await infobipAxios.post('/2fa/2/applications', { name: APP_NAME, configuration: { pinAttempts: 10, // Max attempts to verify PIN allowMultiplePinVerifications: true, // Useful for testing, maybe false in prod pinTimeToLive: '5m', // PIN validity duration (e.g., 5 minutes) verifyPinLimit: '3/1s', // Rate limit for verification calls per second sendPinPerApplicationLimit: '10000/1d', // Max PINs sent per app per day sendPinPerPhoneNumberLimit: '5/1d' // Max PINs sent per phone number per day }, enabled: true }); console.log('Infobip Application created successfully.'); return response.data.applicationId; } catch (error) { console.error('Error creating Infobip Application:', error.response?.data || error.message); // Possible improvement: Check if error indicates a conflict (app already exists) // and attempt to retrieve the existing app's ID. Requires checking Infobip API error codes. throw new Error(`Failed to create Infobip Application: ${error.message}`); } } /** * Creates a 2FA Message Template in Infobip for a given Application. * Lacks idempotency check: Assumes it needs creation if ID is missing. * In production, consider listing templates for the app first. * @param {string} appId The Infobip Application ID. * @returns {Promise<string>} The Message ID. */ async function createMessageTemplate(appId) { try { console.log(`Attempting to create Infobip Message Template for App ID: ${appId}`); // Note: This doesn't check if a template with this configuration already exists for the app. // Running this multiple times without saving the ID will create duplicates. const response = await infobipAxios.post(`/2fa/2/applications/${appId}/messages`, { messageText: MESSAGE_TEMPLATE_TEXT, pinType: 'NUMERIC', // Can be NUMERIC, ALPHA, ALPHANUMERIC, HEX pinLength: 6, // Length of the OTP code language: 'en', // Optional: Specify language senderId: SENDER_ID, // Alphanumeric sender ID (must be registered/approved by Infobip in some regions) // regional: { indiaDlt: { contentTemplateId: '...', principalEntityId: '...' } }, // Example for regional compliance like India DLT }); console.log('Infobip Message Template created successfully.'); return response.data.messageId; } catch (error) { console.error('Error creating Infobip Message Template:', error.response?.data || error.message); // Possible improvement: Check for conflict errors. throw new Error(`Failed to create Infobip Message Template: ${error.message}`); } } // --- Core Service Functions --- /** * Initializes the service: Checks credentials, creates Axios instance, * and ensures Application and Message Template IDs are available, * creating them via API if not found in environment variables. * Relies on manual update of .env after first run. */ async function initialize() { if (!INFOBIP_API_KEY || !INFOBIP_BASE_URL) { throw new Error('Infobip API Key or Base URL missing in environment variables (INFOBIP_API_KEY, INFOBIP_BASE_URL).'); } // Initialize Axios instance now that we have base URL and key infobipAxios = axios.create({ baseURL: INFOBIP_BASE_URL, headers: { 'Authorization': `App ${INFOBIP_API_KEY}`, 'Content-Type': 'application/json', 'Accept': 'application/json' } }); if (!applicationId) { console.log('INFOBIP_APPLICATION_ID not found in .env, attempting to create one...'); applicationId = await createApplication(); // IMPORTANT: Manual step required after first run! console.warn(`------------------------------------------------------------------------------------`); console.warn(`ACTION REQUIRED: Copy the Application ID below and paste it into your .env file as:`); console.warn(`INFOBIP_APPLICATION_ID=${applicationId}`); console.warn(`This avoids creating a new application on every server start.`); console.warn(`------------------------------------------------------------------------------------`); // Consider a dedicated setup script or making these mandatory env vars for subsequent runs. } else { console.log(`Using existing Infobip Application ID from .env: ${applicationId}`); } if (!messageId) { console.log('INFOBIP_MESSAGE_ID not found in .env, attempting to create one...'); messageId = await createMessageTemplate(applicationId); // IMPORTANT: Manual step required after first run! console.warn(`------------------------------------------------------------------------------------`); console.warn(`ACTION REQUIRED: Copy the Message ID below and paste it into your .env file as:`); console.warn(`INFOBIP_MESSAGE_ID=${messageId}`); console.warn(`This avoids creating a new message template on every server start.`); console.warn(`------------------------------------------------------------------------------------`); } else { console.log(`Using existing Infobip Message ID from .env: ${messageId}`); } } /** * Sends an OTP to the specified phone number using the configured App/Message. * @param {string} phoneNumber The recipient's phone number in E.164 format (e.g., 447... ). * @returns {Promise<string>} The PIN ID for verification. */ async function sendOtp(phoneNumber) { if (!applicationId || !messageId || !infobipAxios) { throw new Error('Infobip service not initialized properly. Check configuration and initialization logs.'); } try { const response = await infobipAxios.post('/2fa/2/pin', { applicationId: applicationId, messageId: messageId, // 'from' can often be omitted if set correctly in the Message Template // from: SENDER_ID, to: phoneNumber }); // Response includes pinId, to, ncStatus, smsStatus console.log('Infobip Send PIN Response Status:', response.data.smsStatus); if (!response.data.pinId) { throw new Error('Infobip did not return a pinId in the response.'); } return response.data.pinId; } catch (error) { console.error(`Error sending OTP via Infobip to ${phoneNumber}:`, error.response?.data || error.message); // Rethrow a more specific error or handle based on Infobip error codes throw error; // Let the route handler manage the HTTP response } } /** * Verifies the OTP code submitted by the user against the pinId. * @param {string} pinId The PIN ID received after sending the OTP. * @param {string} otpCode The 6-digit code entered by the user. * @returns {Promise<boolean>} True if verification is successful, false otherwise. */ async function verifyOtp(pinId, otpCode) { if (!applicationId || !messageId || !infobipAxios) { throw new Error('Infobip service not initialized properly. Check configuration and initialization logs.'); } try { const response = await infobipAxios.post(`/2fa/2/pin/${pinId}/verify`, { pin: otpCode }); // Response includes verified (boolean), msisdn, attemptsRemaining console.log(`Infobip Verify PIN Response: Verified=${response.data.verified}, Attempts Left=${response.data.attemptsRemaining}`); return response.data.verified; } catch (error) { console.error(`Error verifying OTP via Infobip for pinId ${pinId}:`, error.response?.data || error.message); // Handle specific errors like PIN expired, not found, etc. if (error.response?.status === 400) { // Common client errors (wrong PIN, expired, max attempts) const serviceException = error.response.data?.requestError?.serviceException; if (serviceException) { console.warn(`Infobip Verification Error: ${serviceException.messageId} - ${serviceException.text}`); // If PIN is incorrect ('PIN_INVALID'), the `verified` flag will be false, caught below. // Re-throw specific errors that the route handler should know about (like expired) if (serviceException.messageId === 'PIN_EXPIRED' || serviceException.messageId === 'PIN_NOT_FOUND' || serviceException.messageId === 'MAX_ATTEMPTS_EXCEEDED') { throw error; // Let route handler provide specific message } } // Treat other 400s (like potentially wrong PIN if not caught above) as simply """"not verified"""" return false; } // Rethrow unexpected errors (e.g., 5xx from Infobip, network errors) for server-level handling throw error; } } module.exports = { initialize, sendOtp, verifyOtp };
-
Create
.env
File: Add your Infobip credentials and configuration here.# .env # --- Server --- PORT=3000 # --- Infobip Credentials --- # Get these from your Infobip account dashboard (https://portal.infobip.com/) # API Key Management section INFOBIP_API_KEY=YOUR_ACTUAL_INFOBIP_API_KEY INFOBIP_BASE_URL=YOUR_INFOBIP_ACCOUNT_BASE_URL # e.g., https://xyz123.api.infobip.com # --- Infobip 2FA Configuration (Leave blank initially) --- # After the first successful server start, the Application ID and Message ID will be logged # to the console. Copy and paste those values here to prevent recreating them on every run. # This is crucial for consistent behavior and avoiding duplicate resources in Infobip. INFOBIP_APPLICATION_ID= INFOBIP_MESSAGE_ID= # --- Optional Infobip Configuration --- # You can override the defaults in infobip.js by setting these variables # INFOBIP_APP_NAME=""My Custom App Name"" # INFOBIP_SENDER_ID=""MySender"" # Ensure this sender is approved/valid in your region # INFOBIP_MESSAGE_TEMPLATE=""Your code for My Custom App is {{pin}}.""
How to find
INFOBIP_API_KEY
andINFOBIP_BASE_URL
:- Log in to your Infobip account: https://portal.infobip.com/
- Your Base URL is usually visible on the homepage dashboard after logging in, often in an ""API"" section or code examples. It looks like
https://<your-unique-id>.api.infobip.com
. - Navigate to the ""API Key Management"" section (often under your account settings or Developer Tools).
- Create a new API key if you don't have one. Copy the key value securely into your
.env
file. Treat this key like a password.
2. Implementing Core Functionality
The core logic is implemented within server.js
(route handlers) and services/infobip.js
(API interaction).
Key Implementation Details:
- Infobip Service Initialization (
initialize
):- On server startup, this function checks if
INFOBIP_APPLICATION_ID
andINFOBIP_MESSAGE_ID
are present in the environment variables (.env
). - If not found, it calls the Infobip API to create a new 2FA Application and a Message Template within that application using the settings defined (or defaulted) in
services/infobip.js
. - It logs the created IDs with clear instructions (ACTION REQUIRED) prompting you to add them to your
.env
file. This manual step is necessary for this example setup to prevent creating new apps/templates on every restart, which is inefficient, costly, and can clutter your Infobip account. A more advanced setup might use a separate one-time setup script. - Why? Application and Message Template configurations define how OTPs are handled (length, expiry, rate limits, message text). These are typically static configurations for your application's 2FA use case.
- On server startup, this function checks if
- Requesting OTP (
/request-otp
route,sendOtp
service function):- The route takes the
phoneNumber
from the request body. - It calls
infobipService.sendOtp
, passing the phone number. - The
sendOtp
function makes aPOST
request to Infobip's/2fa/2/pin
endpoint, including theapplicationId
,messageId
, and recipient number (to
). - Infobip handles generating the PIN and sending the SMS based on the template associated with the
messageId
. - Infobip returns a
pinId
in the response. ThispinId
uniquely identifies this specific OTP request. - Crucially: The
server.js
stores thispinId
temporarily (in our demootpStore
), associating it with thephoneNumber
. This in-memory store is NOT SUITABLE FOR PRODUCTION. In a real application, use secure server-side storage like Redis (with TTL), a database table (with TTL and cleanup), or encrypted session data linked to the user attempting verification. This is vital to link the subsequent verification step back to the correct OTP request and user session. The simplesetTimeout
cleanup in the example is also not robust for production.
- The route takes the
- Verifying OTP (
/verify-otp
route,verifyOtp
service function):- The route takes
phoneNumber
and the submittedotp
code. - It retrieves the
pinId
previously stored for thatphoneNumber
from the temporary store. If nopinId
is found (e.g., expired, cleaned up, never requested, or server restarted with in-memory store), it returns an error. - It calls
infobipService.verifyOtp
, passing the retrievedpinId
and the user's submittedotp
code. - The
verifyOtp
function makes aPOST
request to Infobip's/2fa/2/pin/{pinId}/verify
endpoint. - Infobip checks if the provided
otp
matches the one generated for thatpinId
and if it's still valid (not expired, within attempt limits defined in the Application config). - Infobip returns a response including a
verified
boolean flag. - If
verified
is true, the route clears the storedpinId
(it's been successfully used) and returns success. This is where you would typically update the user's status in your database (e.g., markis_phone_verified = true
). - If
verified
is false or an error occurs (like PIN expired, which the service function identifies and potentially re-throws), it returns an appropriate error message.
- The route takes
Alternative Approaches:
- Infobip SDKs: Infobip provides official SDKs for various languages (check their developer portal). These can abstract some of the direct
axios
calls and might offer convenience features. However, understanding the underlying API calls remains beneficial. - State Management: As emphasized above, replace the demo
otpStore
. Redis with key expiry is a common, performant choice. A database table with columns forpinId
,phoneNumber
(oruserId
),expiresAt
, andisVerified
is another solid option, requiring a background job or query condition to handle cleanup of expired entries.
3. Building a Complete API Layer
Our server.js
already defines the basic API layer. Let's discuss enhancements for production.
- Authentication/Authorization:
- The
/request-otp
endpoint might be public (e.g., during phone verification in registration) or require user authentication (e.g., if adding/verifying a phone for an existing logged-in user). Implement checks accordingly. - The
/verify-otp
endpoint implicitly relies on thepinId
being linked to the correct user session or initial request via your chosen state management. Ensure this link is secure and cannot be manipulated. If verifying a logged-in user, double-check the verification action is authorized for that specific user's session.
- The
- Request Validation: Prevent invalid or malicious data from hitting your core logic or the Infobip API.
- Example using
express-validator
:npm install express-validator
// server.js (additions/modifications shown) const { body, validationResult } = require('express-validator'); const express = require('express'); // Ensure express is required if not already const infobipService = require('./services/infobip'); require('dotenv').config(); const app = express(); // Assuming app setup is here or imported const PORT = process.env.PORT || 3000; app.use(express.json()); // --- In-Memory Storage (DEMO ONLY) --- const otpStore = {}; const setupOtpCleanup = (phoneNumber, pinId) => { setTimeout(() => { if (otpStore[phoneNumber] === pinId) { delete otpStore[phoneNumber]; console.log(`Cleaned up expired/unused pinId for ${phoneNumber}`); } }, 10 * 60 * 1000); // 10 minutes }; // --- Validation Middleware --- const validatePhoneNumber = [ body('phoneNumber') .trim() .notEmpty().withMessage('Phone number is required.') .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format. Include country code if necessary.'), // Consider adding custom validation for E.164 format if needed (using libphonenumber-js) ]; const validateVerification = [ body('phoneNumber') .trim() .notEmpty().withMessage('Phone number is required.') .isMobilePhone('any', { strictMode: false }).withMessage('Invalid phone number format.'), body('otp') .trim() .notEmpty().withMessage('OTP is required.') .isLength({ min: 6, max: 6 }).withMessage('OTP must be exactly 6 digits.') .isNumeric().withMessage('OTP must contain only numbers.') ]; // --- Helper to handle validation results --- const handleValidationErrors = (req, res, next) => { const errors = validationResult(req); if (!errors.isEmpty()) { return res.status(400).json({ success: false, errors: errors.array() }); } next(); }; // --- Routes (with validation added) --- app.post('/request-otp', validatePhoneNumber, handleValidationErrors, async (req, res) => { // ... (existing route logic) const { phoneNumber } = req.body; // Already validated try { console.log(`Requesting OTP for ${phoneNumber}`); const pinId = await infobipService.sendOtp(phoneNumber); console.log(`OTP request successful for ${phoneNumber}, pinId: ${pinId}`); otpStore[phoneNumber] = pinId; setupOtpCleanup(phoneNumber, pinId); res.status(200).json({ success: true, message: 'OTP sent successfully.' }); } catch (error) { // ... (existing error handling) console.error('Error sending OTP:', error.response?.data || error.message); res.status(500).json({ success: false, error: 'Failed to send OTP. Please try again later.', errorCode: error.response?.data?.requestError?.serviceException?.messageId || 'UNKNOWN_ERROR' }); } }); app.post('/verify-otp', validateVerification, handleValidationErrors, async (req, res) => { // ... (existing route logic) const { phoneNumber, otp } = req.body; // Already validated const pinId = otpStore[phoneNumber]; if (!pinId) { // ... (existing handling for missing pinId) console.warn(`No active OTP request found for ${phoneNumber}.`); return res.status(400).json({ success: false, error: 'No active OTP request found or it has expired. Please request a new OTP.' }); } try { // ... (existing verification logic) console.log(`Verifying OTP ${otp} for ${phoneNumber} with pinId ${pinId}`); const isVerified = await infobipService.verifyOtp(pinId, otp); if (isVerified) { // ... (existing success handling) console.log(`OTP verification successful for ${phoneNumber}`); delete otpStore[phoneNumber]; res.status(200).json({ success: true, message: 'OTP verified successfully.' }); } else { // ... (existing failure handling) console.warn(`OTP verification failed for ${phoneNumber}. Incorrect PIN.`); res.status(400).json({ success: false, error: 'Invalid OTP code.' }); } } catch (error) { // ... (existing error handling, including specific error codes) console.error('Error verifying OTP:', error.response?.data || error.message); const errorCode = error.response?.data?.requestError?.serviceException?.messageId; let errorMessage = 'Failed to verify OTP. Please try again later.'; // ... (logic for specific error messages based on errorCode) if (errorCode === 'TOO_MANY_REQUESTS') { errorMessage = 'Too many verification attempts. Please try again later.'; } else if (errorCode === 'PIN_EXPIRED') { errorMessage = 'OTP code has expired. Please request a new one.'; if (otpStore[phoneNumber] === pinId) delete otpStore[phoneNumber]; } else if (errorCode === 'PIN_NOT_FOUND') { errorMessage = 'Invalid request. Please request a new OTP.'; if (otpStore[phoneNumber] === pinId) delete otpStore[phoneNumber]; } res.status(500).json({ success: false, error: errorMessage, errorCode: errorCode || 'UNKNOWN_ERROR' }); } }); // --- Start Server --- app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); infobipService.initialize() .then(() => console.log('Infobip service initialized.')) .catch(err => console.error('Failed to initialize Infobip service:', err)); });
- Example using