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