- Client: API-Basis-URL (joinApiUrl, /v1-Falle), Vite strictPort + Proxy 127.0.0.1, Nicht-JSON-Fehler - Server: /api-404 ohne Wildcard-Bug, SPA-Fallback, Auth-Middleware, Cron, Mailer, Crypto - Routen: OAuth-State, Email/Stripe/Analytics; client/.env.example Made-with: Cursor
105 lines
2.6 KiB
JavaScript
105 lines
2.6 KiB
JavaScript
/**
|
|
* Rate Limiting Middleware
|
|
* Prevents abuse by limiting requests per IP/user
|
|
*/
|
|
|
|
import { RateLimitError } from './errorHandler.mjs'
|
|
import { isAdmin } from '../config/index.mjs'
|
|
|
|
// In-memory store for rate limiting (use Redis in production)
|
|
const requestCounts = new Map()
|
|
|
|
// Clean up old entries every minute
|
|
setInterval(() => {
|
|
const now = Date.now()
|
|
for (const [key, data] of requestCounts.entries()) {
|
|
if (now - data.windowStart > data.windowMs) {
|
|
requestCounts.delete(key)
|
|
}
|
|
}
|
|
}, 60000)
|
|
|
|
/**
|
|
* Create rate limiter middleware
|
|
* @param {Object} options - Rate limit options
|
|
* @param {number} options.windowMs - Time window in milliseconds
|
|
* @param {number} options.max - Max requests per window
|
|
* @param {string} options.message - Error message
|
|
* @param {Function} options.keyGenerator - Function to generate unique key
|
|
* @param {Function} options.skip - If (req) => true, do not count this request
|
|
*/
|
|
export function rateLimit(options = {}) {
|
|
const {
|
|
windowMs = 60000, // 1 minute
|
|
max = 100,
|
|
message = 'Zu viele Anfragen. Bitte versuche es später erneut.',
|
|
keyGenerator = (req) => req.ip,
|
|
skip = () => false,
|
|
} = options
|
|
|
|
return (req, res, next) => {
|
|
if (skip(req)) {
|
|
return next()
|
|
}
|
|
|
|
const key = keyGenerator(req)
|
|
const now = Date.now()
|
|
|
|
let data = requestCounts.get(key)
|
|
|
|
if (!data || now - data.windowStart > windowMs) {
|
|
data = { count: 0, windowStart: now, windowMs }
|
|
requestCounts.set(key, data)
|
|
}
|
|
|
|
data.count++
|
|
|
|
// Set rate limit headers
|
|
res.set({
|
|
'X-RateLimit-Limit': max,
|
|
'X-RateLimit-Remaining': Math.max(0, max - data.count),
|
|
'X-RateLimit-Reset': new Date(data.windowStart + windowMs).toISOString(),
|
|
})
|
|
|
|
if (data.count > max) {
|
|
return next(new RateLimitError(message))
|
|
}
|
|
|
|
next()
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Pre-configured rate limiters
|
|
*/
|
|
export const limiters = {
|
|
// General API rate limit
|
|
api: rateLimit({
|
|
windowMs: 60000,
|
|
max: 100,
|
|
message: 'API Rate Limit überschritten',
|
|
}),
|
|
|
|
// Stricter limit for auth endpoints
|
|
auth: rateLimit({
|
|
windowMs: 900000, // 15 minutes
|
|
max: 10,
|
|
message: 'Zu viele Anmeldeversuche. Bitte warte 15 Minuten.',
|
|
}),
|
|
|
|
// Limit for email sorting (expensive operation); ADMIN_EMAILS (isAdmin) bypass
|
|
emailSort: rateLimit({
|
|
windowMs: 60000,
|
|
max: 30, // Erhöht für Entwicklung
|
|
message: 'E-Mail-Sortierung ist limitiert. Bitte warte eine Minute.',
|
|
skip: (req) => isAdmin(req.appwriteUser?.email),
|
|
}),
|
|
|
|
// Limit for AI operations
|
|
ai: rateLimit({
|
|
windowMs: 60000,
|
|
max: 20,
|
|
message: 'KI-Anfragen sind limitiert.',
|
|
}),
|
|
}
|