293 lines
8.9 KiB
JavaScript
293 lines
8.9 KiB
JavaScript
const rateLimit = require("express-rate-limit");
|
|
const logger = require("../utils/logger");
|
|
|
|
// 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."
|
|
);
|
|
|
|
// Upload presign rate limiter - 30 requests per minute
|
|
const uploadPresignLimiter = createUserBasedRateLimiter(
|
|
60 * 1000, // 1 minute window
|
|
30, // 30 presign requests per minute per user
|
|
"Too many upload requests. Please slow down."
|
|
);
|
|
|
|
// Helper to create a rate limit handler that logs the event
|
|
const createRateLimitHandler = (limiterName) => (req, res, next, options) => {
|
|
const reqLogger = logger.withRequestId(req.id);
|
|
reqLogger.warn('Rate limit exceeded', {
|
|
limiter: limiterName,
|
|
ip: req.ip,
|
|
userId: req.user?.id || 'anonymous',
|
|
method: req.method,
|
|
url: req.url,
|
|
userAgent: req.get('User-Agent'),
|
|
message: options.message?.error || 'Rate limit exceeded'
|
|
});
|
|
res.status(options.statusCode).json(options.message);
|
|
};
|
|
|
|
// 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
|
|
handler: createRateLimitHandler('login'),
|
|
}),
|
|
|
|
// 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,
|
|
handler: createRateLimitHandler('register'),
|
|
}),
|
|
|
|
// 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,
|
|
handler: createRateLimitHandler('passwordReset'),
|
|
}),
|
|
|
|
// Alpha code validation rate limiter
|
|
alphaCodeValidation: rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 5, // 5 code validation attempts per 15 minutes
|
|
message: {
|
|
error: "Too many attempts. Please try again later.",
|
|
retryAfter: 900,
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
handler: createRateLimitHandler('alphaCodeValidation'),
|
|
}),
|
|
|
|
// Email verification rate limiter - protect against brute force on 6-digit codes
|
|
emailVerification: rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 10, // 10 verification attempts per 15 minutes per IP
|
|
message: {
|
|
error: "Too many verification attempts. Please try again later.",
|
|
retryAfter: 900,
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
handler: createRateLimitHandler('emailVerification'),
|
|
}),
|
|
|
|
// 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,
|
|
handler: createRateLimitHandler('general'),
|
|
}),
|
|
|
|
// Two-Factor Authentication rate limiters
|
|
twoFactorVerification: rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 10, // 10 verification attempts per 15 minutes
|
|
message: {
|
|
error: "Too many verification attempts. Please try again later.",
|
|
retryAfter: 900,
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
skipSuccessfulRequests: true,
|
|
handler: createRateLimitHandler('twoFactorVerification'),
|
|
}),
|
|
|
|
twoFactorSetup: rateLimit({
|
|
windowMs: 60 * 60 * 1000, // 1 hour
|
|
max: 5, // 5 setup attempts per hour
|
|
message: {
|
|
error: "Too many setup attempts. Please try again later.",
|
|
retryAfter: 3600,
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
handler: createRateLimitHandler('twoFactorSetup'),
|
|
}),
|
|
|
|
recoveryCode: rateLimit({
|
|
windowMs: 15 * 60 * 1000, // 15 minutes
|
|
max: 3, // 3 recovery code attempts per 15 minutes
|
|
message: {
|
|
error: "Too many recovery code attempts. Please try again later.",
|
|
retryAfter: 900,
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
skipSuccessfulRequests: false, // Count all attempts for security
|
|
handler: createRateLimitHandler('recoveryCode'),
|
|
}),
|
|
|
|
emailOtpSend: rateLimit({
|
|
windowMs: 10 * 60 * 1000, // 10 minutes
|
|
max: 2, // 2 OTP sends per 10 minutes
|
|
message: {
|
|
error: "Please wait before requesting another code.",
|
|
retryAfter: 600,
|
|
},
|
|
standardHeaders: true,
|
|
legacyHeaders: false,
|
|
handler: createRateLimitHandler('emailOtpSend'),
|
|
}),
|
|
};
|
|
|
|
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,
|
|
alphaCodeValidationLimiter: authRateLimiters.alphaCodeValidation,
|
|
emailVerificationLimiter: authRateLimiters.emailVerification,
|
|
generalLimiter: authRateLimiters.general,
|
|
|
|
// Two-Factor Authentication rate limiters
|
|
twoFactorVerificationLimiter: authRateLimiters.twoFactorVerification,
|
|
twoFactorSetupLimiter: authRateLimiters.twoFactorSetup,
|
|
recoveryCodeLimiter: authRateLimiters.recoveryCode,
|
|
emailOtpSendLimiter: authRateLimiters.emailOtpSend,
|
|
|
|
// Burst protection
|
|
burstProtection,
|
|
|
|
// Upload rate limiter
|
|
uploadPresignLimiter,
|
|
|
|
// Utility functions
|
|
createMapsRateLimiter,
|
|
createUserBasedRateLimiter,
|
|
};
|