Files
beauty-bookings/server-dist/lib/rate-limiter.js
elpatron fbfdceeee6 feat: CalDAV-Integration für Admin-Kalender
- Neue CalDAV-Route mit PROPFIND und GET-Endpoints
- ICS-Format-Generator für Buchungsdaten
- Token-basierte Authentifizierung für CalDAV-Zugriff
- Admin-Interface mit CalDAV-Link-Generator
- Schritt-für-Schritt-Anleitung für Kalender-Apps
- 24h-Token-Ablaufzeit für Sicherheit
- Unterstützung für Outlook, Google Calendar, Apple Calendar, Thunderbird

Fixes: Admin kann jetzt Terminkalender in externen Apps abonnieren
2025-10-06 12:41:50 +02:00

118 lines
3.5 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 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;
}