226 lines
6.8 KiB
JavaScript
226 lines
6.8 KiB
JavaScript
// 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,
|
|
};
|
|
}
|