// 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 get = (name) => { if (typeof headers.get === 'function') { // Headers interface const v = headers.get(name); return v === null ? undefined : v; } return headers[name]; }; const forwardedFor = get('x-forwarded-for'); if (forwardedFor) { // x-forwarded-for can contain multiple IPs, take the first one return forwardedFor.split(',')[0].trim(); } const realIP = get('x-real-ip'); if (realIP) { return realIP; } const cfConnectingIP = get('cf-connecting-ip'); // Cloudflare if (cfConnectingIP) { return cfConnectingIP; } // No IP found return undefined; } /** * Reset a rate limit entry immediately (e.g., after successful login) */ export function resetRateLimit(key) { rateLimitStore.delete(key); } /** * Convenience helper to reset login attempts for an IP */ export function resetLoginRateLimit(ip) { if (!ip) return; resetRateLimit(`login:ip:${ip}`); } import { getSessionFromCookies } from "./auth.js"; /** * Enforce admin rate limiting by IP and user. Throws standardized German error on exceed. */ export async function enforceAdminRateLimit(context) { const ip = getClientIP(context.req.raw.headers); const session = await getSessionFromCookies(context); if (!session) return; // No session -> owner assertion elsewhere; no per-user throttling const result = checkAdminRateLimit({ ip, userId: session.userId }); if (!result.allowed) { throw new Error(`Zu viele Admin-Anfragen. Bitte versuche es in ${result.retryAfterSeconds} Sekunden erneut.`); } } /** * Brute-Force-Schutz für Logins (IP-basiert) * * Konfiguration: * - max. 5 Versuche je IP in 15 Minuten * * Schlüssel: "login:ip:${ip}" */ export function checkLoginRateLimit(ip) { // Wenn keine IP ermittelbar ist, erlauben (kein Tracking möglich) if (!ip) { return { allowed: true, remaining: 5, resetAt: Date.now() + 15 * 60 * 1000, }; } const loginConfig = { maxRequests: 5, windowMs: 15 * 60 * 1000, // 15 Minuten }; const key = `login:ip:${ip}`; return checkRateLimit(key, loginConfig); } /** * Rate Limiting für Admin-Operationen * * Konfigurationen (beide Checks werden geprüft, restriktiverer gewinnt): * - Benutzer-basiert: 30 Anfragen je Benutzer in 5 Minuten * - IP-basiert: 50 Anfragen je IP in 5 Minuten * * Schlüssel: * - "admin:user:${userId}" * - "admin:ip:${ip}" */ export function checkAdminRateLimit(params) { const { ip, userId } = params; const userConfig = { maxRequests: 30, windowMs: 5 * 60 * 1000, // 5 Minuten }; const ipConfig = { maxRequests: 50, windowMs: 5 * 60 * 1000, // 5 Minuten }; const userKey = `admin:user:${userId}`; const userResult = checkRateLimit(userKey, userConfig); // Wenn Benutzerlimit bereits überschritten ist, direkt zurückgeben if (!userResult.allowed) { return { ...userResult, allowed: false }; } // Falls IP verfügbar, zusätzlich prüfen if (ip) { const ipKey = `admin:ip:${ip}`; const ipResult = checkRateLimit(ipKey, ipConfig); if (!ipResult.allowed) { return { ...ipResult, allowed: false }; } // Beide Checks erlaubt: restriktivere Restwerte/Reset nehmen return { allowed: true, remaining: Math.min(userResult.remaining, ipResult.remaining), resetAt: Math.min(userResult.resetAt, ipResult.resetAt), }; } // Kein IP-Check möglich return { allowed: true, remaining: userResult.remaining, resetAt: userResult.resetAt, }; }