// src/middleware/rateLimitMiddleware.js // === ADDED FOR RATE LIMITING === // Rate limiting middleware for OTP requests and verification const { getRedisClient, isRedisReady } = require('../services/redisClient'); // In-memory fallback store (used when Redis is not available) // Unified store for all rate limiting keys const memoryStore = {}; // Clean up expired entries from memory store periodically setInterval(() => { const now = Date.now(); Object.keys(memoryStore).forEach((key) => { if (memoryStore[key].expiresAt && memoryStore[key].expiresAt < now) { delete memoryStore[key]; } }); }, 60000); // Clean up every minute // Configuration from environment variables const config = { // OTP request limits OTP_REQ_PHONE_10MIN_LIMIT: parseInt(process.env.OTP_REQ_PHONE_10MIN_LIMIT || '3', 10), OTP_REQ_PHONE_DAY_LIMIT: parseInt(process.env.OTP_REQ_PHONE_DAY_LIMIT || '10', 10), OTP_REQ_IP_10MIN_LIMIT: parseInt(process.env.OTP_REQ_IP_10MIN_LIMIT || '20', 10), OTP_REQ_IP_DAY_LIMIT: parseInt(process.env.OTP_REQ_IP_DAY_LIMIT || '100', 10), // OTP verification limits OTP_VERIFY_MAX_ATTEMPTS: parseInt(process.env.OTP_VERIFY_MAX_ATTEMPTS || '5', 10), OTP_VERIFY_FAILED_PER_HOUR_LIMIT: parseInt(process.env.OTP_VERIFY_FAILED_PER_HOUR_LIMIT || '10', 10), // OTP validity OTP_TTL_SECONDS: parseInt(process.env.OTP_TTL_SECONDS || '120', 10), // 2 minutes }; /** * Helper: Increment counter in Redis or memory store */ async function incrementCounter(key, ttlSeconds) { const redis = await getRedisClient(); if (isRedisReady() && redis) { try { const count = await redis.incr(key); if (count === 1) { // First increment, set TTL await redis.expire(key, ttlSeconds); } return count; } catch (err) { console.error('Redis increment error, falling back to memory:', err); // Fall through to memory store } } // Memory store fallback const now = Date.now(); if (!memoryStore[key]) { memoryStore[key] = { count: 0, expiresAt: now + ttlSeconds * 1000, }; } memoryStore[key].count++; return memoryStore[key].count; } /** * Helper: Get counter value from Redis or memory store */ async function getCounter(key) { const redis = await getRedisClient(); if (isRedisReady() && redis) { try { const count = await redis.get(key); return count ? parseInt(count, 10) : 0; } catch (err) { console.error('Redis get error, falling back to memory:', err); // Fall through to memory store } } // Memory store fallback if (memoryStore[key] && memoryStore[key].expiresAt > Date.now()) { return memoryStore[key].count || 0; } return 0; } /** * Helper: Check if key exists in Redis or memory store */ async function exists(key) { const redis = await getRedisClient(); if (isRedisReady() && redis) { try { const result = await redis.exists(key); return result === 1; } catch (err) { console.error('Redis exists error, falling back to memory:', err); // Fall through to memory store } } // Memory store fallback if (memoryStore[key] && memoryStore[key].expiresAt > Date.now()) { return true; } return false; } /** * Helper: Set key with TTL in Redis or memory store */ async function setWithTTL(key, value, ttlSeconds) { const redis = await getRedisClient(); if (isRedisReady() && redis) { try { await redis.setEx(key, ttlSeconds, value); return true; } catch (err) { console.error('Redis setEx error, falling back to memory:', err); // Fall through to memory store } } // Memory store fallback memoryStore[key] = { value, expiresAt: Date.now() + ttlSeconds * 1000, }; return true; } /** * === ADDED FOR 2-MIN OTP VALIDITY & NO-RESEND === * Middleware: Check if there's an active OTP for the phone number * Prevents sending a new OTP if one is already active (within 2 minutes) */ async function checkActiveOtpForPhone(req, res, next) { try { const { phone_number } = req.body; if (!phone_number) { return next(); // Let validation handle this } // Normalize phone (same logic as in routes) const normalizedPhone = phone_number.trim().replace(/\s+/g, ''); const phone = normalizedPhone.startsWith('+') ? normalizedPhone : (normalizedPhone.length === 10 ? '+91' + normalizedPhone : normalizedPhone); const activeOtpKey = `otp_active:phone:${phone}`; const hasActiveOtp = await exists(activeOtpKey); if (hasActiveOtp) { return res.status(429).json({ success: false, message: 'An OTP is already active. Please wait a moment before requesting a new one.', }); } next(); } catch (err) { console.error('checkActiveOtpForPhone error:', err); // On error, allow the request to proceed (fail open) next(); } } /** * === ADDED FOR RATE LIMITING === * Middleware: Rate limit OTP requests per phone number */ async function rateLimitRequestOtpByPhone(req, res, next) { try { const { phone_number } = req.body; if (!phone_number) { return next(); // Let validation handle this } // Normalize phone (same logic as in routes) const normalizedPhone = phone_number.trim().replace(/\s+/g, ''); const phone = normalizedPhone.startsWith('+') ? normalizedPhone : (normalizedPhone.length === 10 ? '+91' + normalizedPhone : normalizedPhone); // Check 10-minute limit const key10min = `otp_req:phone:${phone}:10min`; const count10min = await incrementCounter(key10min, 600); // 10 minutes = 600 seconds if (count10min > config.OTP_REQ_PHONE_10MIN_LIMIT) { return res.status(429).json({ success: false, message: 'Too many OTP requests. Please try again later.', }); } // Check 24-hour limit const keyDay = `otp_req:phone:${phone}:day`; const countDay = await incrementCounter(keyDay, 86400); // 24 hours = 86400 seconds if (countDay > config.OTP_REQ_PHONE_DAY_LIMIT) { return res.status(429).json({ success: false, message: 'Too many OTP requests. Please try again later.', }); } next(); } catch (err) { console.error('rateLimitRequestOtpByPhone error:', err); // On error, allow the request to proceed (fail open) next(); } } /** * === ADDED FOR RATE LIMITING === * Middleware: Rate limit OTP requests per IP address */ async function rateLimitRequestOtpByIp(req, res, next) { try { // Get client IP (considering proxies) const ip = req.ip || req.connection.remoteAddress || 'unknown'; // Check 10-minute limit const key10min = `otp_req:ip:${ip}:10min`; const count10min = await incrementCounter(key10min, 600); // 10 minutes = 600 seconds if (count10min > config.OTP_REQ_IP_10MIN_LIMIT) { return res.status(429).json({ success: false, message: 'Too many OTP requests. Please try again later.', }); } // Check 24-hour limit const keyDay = `otp_req:ip:${ip}:day`; const countDay = await incrementCounter(keyDay, 86400); // 24 hours = 86400 seconds if (countDay > config.OTP_REQ_IP_DAY_LIMIT) { return res.status(429).json({ success: false, message: 'Too many OTP requests. Please try again later.', }); } next(); } catch (err) { console.error('rateLimitRequestOtpByIp error:', err); // On error, allow the request to proceed (fail open) next(); } } /** * === ADDED FOR OTP ATTEMPT LIMIT === * Middleware: Rate limit failed OTP verification attempts per phone */ async function rateLimitVerifyOtpByPhone(req, res, next) { try { const { phone_number } = req.body; if (!phone_number) { return next(); // Let validation handle this } // Normalize phone (same logic as in routes) const normalizedPhone = phone_number.trim().replace(/\s+/g, ''); const phone = normalizedPhone.startsWith('+') ? normalizedPhone : (normalizedPhone.length === 10 ? '+91' + normalizedPhone : normalizedPhone); // Check failed verification limit per hour const key = `otp_verify_failed:phone:${phone}:hour`; const count = await getCounter(key); if (count >= config.OTP_VERIFY_FAILED_PER_HOUR_LIMIT) { return res.status(429).json({ success: false, message: 'Too many attempts. Please try again later.', }); } next(); } catch (err) { console.error('rateLimitVerifyOtpByPhone error:', err); // On error, allow the request to proceed (fail open) next(); } } /** * Helper: Mark active OTP (called after OTP is created) */ async function markActiveOtp(phone, ttlSeconds) { const key = `otp_active:phone:${phone}`; await setWithTTL(key, '1', ttlSeconds); } /** * Helper: Increment failed verification counter */ async function incrementFailedVerify(phone) { const key = `otp_verify_failed:phone:${phone}:hour`; await incrementCounter(key, 3600); // 1 hour = 3600 seconds } module.exports = { checkActiveOtpForPhone, rateLimitRequestOtpByPhone, rateLimitRequestOtpByIp, rateLimitVerifyOtpByPhone, markActiveOtp, incrementFailedVerify, config, };