Files
beauty-bookings/server-dist/lib/rate-limiter.js

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,
};
}