Terminplaner: Kunden-Schnittstelle – Nachbesserungen

- 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>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-03 08:17:43 +00:00
1 parent e3e198fb1a
commit bb45d35ec7
8 files changed
+319 -65

No files matched your search

+5 -22
View File
@@ -7,11 +7,10 @@ import { uploadSlot } from './chat.js'
import { config } from '../config.js'
import { getDocument } from '../services/appwriteAdmin.js'
import { logIfKundenansicht } from '../services/kundenansicht.js'
import { createWriteLimiter } from '../services/writeLimiter.js'
const router = Router()
const HOUR_MS = 60 * 60 * 1000
const MAX_WRITES_PER_HOUR = 30
const IMAGE_FILE_RE = /^([a-z0-9-]+)-(l|s)\.webp$/
function fail(res, err) {
@@ -23,26 +22,10 @@ function fail(res, err) {
}
// Schreib-Limit je Kunde und Stunde (im Speicher, wie beim Chat); zaehlt POST/PATCH/DELETE, nicht die Foto-Uploads
const recentWrites = new Map()
function writeRateLimited(customerId) {
const now = Date.now()
for (const [key, times] of recentWrites) {
const live = times.filter((t) => now - t < HOUR_MS)
if (live.length === 0) recentWrites.delete(key)
else if (live.length !== times.length) recentWrites.set(key, live)
}
const list = recentWrites.get(customerId) || []
list.push(now)
recentWrites.set(customerId, list)
return list.length > MAX_WRITES_PER_HOUR
}
function limitWrites(req, res, next) {
if (writeRateLimited(getSessionCustomerId(req))) {
return res.status(429).json({ error: 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.' })
}
return next()
}
const limitWrites = createWriteLimiter({ max: 30 }).middleware(
(req) => getSessionCustomerId(req),
'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.'
)
// Protokoll-Detail der Kundenansicht: Titel, sonst der Anfang des Textes
const postLabel = (post) => String(post?.title || '').trim() || String(post?.text || '').slice(0, 60)