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
This commit is contained in:
117
server-dist/lib/rate-limiter.js
Normal file
117
server-dist/lib/rate-limiter.js
Normal file
@@ -0,0 +1,117 @@
|
||||
// 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;
|
||||
}
|
Reference in New Issue
Block a user