327 lines
9.1 KiB
JavaScript
327 lines
9.1 KiB
JavaScript
// 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,
|
|
};
|
|
|