// Simple in-memory rate limiter for IP and email-based requests // For production with multiple instances, consider using Redis const rateLimitStore = new Map(); // Cleanup old entries every 10 minutes to prevent memory leaks setInterval(() => { const now = Date.now(); for (const [key, entry] of rateLimitStore.entries()) { if (entry.resetAt < now) { rateLimitStore.delete(key); } } }, 10 * 60 * 1000); /** * Check if a request is allowed based on rate limiting * @param key - Unique identifier (IP, email, or combination) * @param config - Rate limit configuration * @returns RateLimitResult with allow status and metadata */ export function checkRateLimit(key, config) { const now = Date.now(); const entry = rateLimitStore.get(key); // No existing entry or window expired - allow and create new entry if (!entry || entry.resetAt < now) { rateLimitStore.set(key, { count: 1, resetAt: now + config.windowMs, }); return { allowed: true, remaining: config.maxRequests - 1, resetAt: now + config.windowMs, }; } // Existing entry within window if (entry.count >= config.maxRequests) { // Rate limit exceeded const retryAfterSeconds = Math.ceil((entry.resetAt - now) / 1000); return { allowed: false, remaining: 0, resetAt: entry.resetAt, retryAfterSeconds, }; } // Increment count and allow entry.count++; rateLimitStore.set(key, entry); return { allowed: true, remaining: config.maxRequests - entry.count, resetAt: entry.resetAt, }; } /** * Check rate limit for booking creation * Applies multiple checks: per IP and per email */ export function checkBookingRateLimit(params) { const { ip, email } = params; // Config: max 3 bookings per email per hour const emailConfig = { maxRequests: 3, windowMs: 60 * 60 * 1000, // 1 hour }; // Config: max 5 bookings per IP per 10 minutes const ipConfig = { maxRequests: 5, windowMs: 10 * 60 * 1000, // 10 minutes }; // Check email rate limit const emailKey = `booking:email:${email.toLowerCase()}`; const emailResult = checkRateLimit(emailKey, emailConfig); if (!emailResult.allowed) { return { ...emailResult, allowed: false, }; } // Check IP rate limit (if IP is available) if (ip) { const ipKey = `booking:ip:${ip}`; const ipResult = checkRateLimit(ipKey, ipConfig); if (!ipResult.allowed) { return { ...ipResult, allowed: false, }; } } // Both checks passed return { allowed: true, remaining: Math.min(emailResult.remaining, ip ? Infinity : emailResult.remaining), resetAt: emailResult.resetAt, }; } /** * Get client IP from various headers (for proxy/load balancer support) */ export function getClientIP(headers) { // Check common proxy headers const forwardedFor = headers['x-forwarded-for']; if (forwardedFor) { // x-forwarded-for can contain multiple IPs, take the first one return forwardedFor.split(',')[0].trim(); } const realIP = headers['x-real-ip']; if (realIP) { return realIP; } const cfConnectingIP = headers['cf-connecting-ip']; // Cloudflare if (cfConnectingIP) { return cfConnectingIP; } // No IP found return undefined; }