- GET /zeiten?ohne=<Buchung>: Leistung, Dauer und Person der Buchung wie beim Verschieben (verschiebeZeiten exportiert), unbekannte/fremde/Sperre 404 (R42) - Schreib-Begrenzung als gemeinsamer Baustein (writeLimiter.js) fuer News und Terminplaner statt kopierter Funktion (R43) - Unlesbares Einstellungs-JSON: Kontext traegt die echte Version, Speichern ueberschreibt den Schaden statt dauerhaft 409 - U+FEFF als Escape statt als rohes Zeichen - Tests: Mandantentrennung mit echtem zweitem Kunden, R42, Limiter, kaputtes JSON Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
42 lines
2.2 KiB
JavaScript
42 lines
2.2 KiB
JavaScript
/**
|
|
* Schreib-Begrenzung je Schluessel (meist Kunde) im Speicher: hoechstens `max` Aufrufe je Fenster (Standard eine Stunde),
|
|
* gezaehlt ab dem Aufruf. Jeder Aufruf zaehlt, auch ein abgelehnter. Wie beim Chat und bei den News nur im Prozess, ein
|
|
* Neustart setzt die Zaehler zurueck; Schluessel ohne Treffer im Fenster werden bei der naechsten Abfrage vergessen.
|
|
* Genutzt von routes/news.js (30 je Stunde) und routes/termine.js (120 je Stunde).
|
|
*
|
|
* @param {{ max: number, fensterMs?: number, now?: () => number }} optionen now liefert Millisekunden (Tests: feste Uhr)
|
|
* @returns {{
|
|
* istUeberschritten: (key: string) => boolean,
|
|
* middleware: (keyOf: (req: object) => string, message: string) => (req: object, res: object, next: () => void) => void,
|
|
* reset: () => void,
|
|
* }} istUeberschritten zaehlt diesen Aufruf mit und sagt, ob damit mehr als `max` im Fenster liegen;
|
|
* middleware antwortet dann 429 { error: message }, sonst next(); reset leert alles (Tests)
|
|
*/
|
|
export function createWriteLimiter({ max, fensterMs = 60 * 60 * 1000, now = () => Date.now() } = {}) {
|
|
if (!Number.isInteger(max) || max < 1) throw new TypeError('createWriteLimiter: max muss eine ganze Zahl ab 1 sein')
|
|
if (!Number.isFinite(fensterMs) || fensterMs <= 0) throw new TypeError('createWriteLimiter: fensterMs muss groesser als 0 sein')
|
|
if (typeof now !== 'function') throw new TypeError('createWriteLimiter: now muss eine Funktion sein')
|
|
|
|
const treffer = new Map() // Schluessel -> Zeitpunkte (ms) im Fenster
|
|
|
|
function istUeberschritten(key) {
|
|
const jetzt = now()
|
|
for (const [schluessel, zeiten] of treffer) {
|
|
const frisch = zeiten.filter((t) => jetzt - t < fensterMs)
|
|
if (frisch.length === 0) treffer.delete(schluessel)
|
|
else if (frisch.length !== zeiten.length) treffer.set(schluessel, frisch)
|
|
}
|
|
const liste = treffer.get(key) || []
|
|
liste.push(jetzt)
|
|
treffer.set(key, liste)
|
|
return liste.length > max
|
|
}
|
|
|
|
const middleware = (keyOf, message) => (req, res, next) => {
|
|
if (istUeberschritten(keyOf(req))) return res.status(429).json({ error: message })
|
|
return next()
|
|
}
|
|
|
|
return { istUeberschritten, middleware, reset: () => treffer.clear() }
|
|
}
|