Files
rentall-app/backend/middleware/rateLimiter.js
2025-09-17 18:37:07 -04:00

178 lines
5.1 KiB
JavaScript

const rateLimit = require("express-rate-limit");
// General rate limiter for Maps API endpoints
const createMapsRateLimiter = (windowMs, max, message) => {
return rateLimit({
windowMs, // time window in milliseconds
max, // limit each IP/user to max requests per windowMs
message: {
error: message,
retryAfter: Math.ceil(windowMs / 1000), // seconds
},
standardHeaders: true, // Return rate limit info in the `RateLimit-*` headers
legacyHeaders: false, // Disable the `X-RateLimit-*` headers
// Use user ID if available, otherwise fall back to IPv6-safe IP handling
keyGenerator: (req) => {
if (req.user?.id) {
return `user:${req.user.id}`;
}
// Use the built-in IP key generator which properly handles IPv6
return rateLimit.defaultKeyGenerator(req);
},
});
};
// Specific rate limiters for different endpoints
const rateLimiters = {
// Places Autocomplete - allow more requests since users type frequently
placesAutocomplete: createMapsRateLimiter(
60 * 1000, // 1 minute window
30, // 30 requests per minute per user/IP
"Too many autocomplete requests. Please slow down."
),
// Place Details - moderate limit since each selection triggers this
placeDetails: createMapsRateLimiter(
60 * 1000, // 1 minute window
20, // 20 requests per minute per user/IP
"Too many place detail requests. Please slow down."
),
// Geocoding - lower limit since this is typically used less frequently
geocoding: createMapsRateLimiter(
60 * 1000, // 1 minute window
10, // 10 requests per minute per user/IP
"Too many geocoding requests. Please slow down."
),
};
// Enhanced rate limiter with user-specific limits
const createUserBasedRateLimiter = (windowMs, max, message) => {
const store = new Map(); // Simple in-memory store (use Redis in production)
return (req, res, next) => {
const key = req.user?.id
? `user:${req.user.id}`
: rateLimit.defaultKeyGenerator(req);
const now = Date.now();
const windowStart = now - windowMs;
// Clean up old entries
for (const [k, data] of store.entries()) {
if (data.windowStart < windowStart) {
store.delete(k);
}
}
// Get or create user's request data
let userData = store.get(key);
if (!userData || userData.windowStart < windowStart) {
userData = {
count: 0,
windowStart: now,
resetTime: now + windowMs,
};
}
// Check if limit exceeded
if (userData.count >= max) {
return res.status(429).json({
error: message,
retryAfter: Math.ceil((userData.resetTime - now) / 1000),
});
}
// Increment counter and store
userData.count++;
store.set(key, userData);
// Add headers
res.set({
"RateLimit-Limit": max,
"RateLimit-Remaining": Math.max(0, max - userData.count),
"RateLimit-Reset": new Date(userData.resetTime).toISOString(),
});
next();
};
};
// Burst protection for expensive operations
const burstProtection = createUserBasedRateLimiter(
10 * 1000, // 10 seconds
5, // 5 requests per 10 seconds
"Too many requests in a short period. Please slow down."
);
// Authentication rate limiters
const authRateLimiters = {
// Login rate limiter - stricter to prevent brute force
login: rateLimit({
windowMs: 15 * 60 * 1000, // 15 minutes
max: 5, // 5 login attempts per 15 minutes
message: {
error: "Too many login attempts. Please try again in 15 minutes.",
retryAfter: 900, // seconds
},
standardHeaders: true,
legacyHeaders: false,
skipSuccessfulRequests: true, // Don't count successful logins
}),
// Registration rate limiter
register: rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3, // 3 registration attempts per hour
message: {
error: "Too many registration attempts. Please try again later.",
retryAfter: 3600,
},
standardHeaders: true,
legacyHeaders: false,
}),
// Password reset rate limiter
passwordReset: rateLimit({
windowMs: 60 * 60 * 1000, // 1 hour
max: 3, // 3 password reset requests per hour
message: {
error: "Too many password reset requests. Please try again later.",
retryAfter: 3600,
},
standardHeaders: true,
legacyHeaders: false,
}),
// General API rate limiter
general: rateLimit({
windowMs: 60 * 1000, // 1 minute
max: 100, // 100 requests per minute
message: {
error: "Too many requests. Please slow down.",
retryAfter: 60,
},
standardHeaders: true,
legacyHeaders: false,
}),
};
module.exports = {
// Individual rate limiters
placesAutocomplete: rateLimiters.placesAutocomplete,
placeDetails: rateLimiters.placeDetails,
geocoding: rateLimiters.geocoding,
// Auth rate limiters
loginLimiter: authRateLimiters.login,
registerLimiter: authRateLimiters.register,
passwordResetLimiter: authRateLimiters.passwordReset,
generalLimiter: authRateLimiters.general,
// Burst protection
burstProtection,
// Utility functions
createMapsRateLimiter,
createUserBasedRateLimiter,
};