178 lines
5.1 KiB
JavaScript
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,
|
|
};
|