import { parsePortalSession } from './session.js' import { VIEW_EXPIRED_MESSAGE, endKundenansicht, isExpired, isKundenansicht, logOnce } from '../services/kundenansicht.js' /** * Waechter der Kundenansicht vor allen Kunden-Routen (Spec 2026-10-01, Abschnitt 3): * 1. abgelaufen (2 h) -> 401 view_expired, Cookies weg; ebenso alte Ansichten ohne viewId ("Als Kunde ansehen" aus * dem frueheren Admin-Panel) * 2. fremde Kennung im Header X-Kundenansicht -> 409 view_replaced (Tab einer anderen Kundenansicht) * 3. Lesen ist erlaubt; Schreiben nur mit passender Kennung und nur, was auf der Erlaubnisliste steht. * Neue Schreib-Routen sind damit in der Kundenansicht gesperrt, bis sie bewusst auf die Liste kommen. */ export const KUNDENANSICHT_ALLOWED = [ ['POST', /^\/api\/news\/?$/], ['POST', /^\/api\/news\/image\/?$/], ['PATCH', /^\/api\/news\/[^/]+\/?$/], ['DELETE', /^\/api\/news\/[^/]+\/?$/], ['POST', /^\/api\/funktionen\/analyse\/heatmap-link\/?$/], // Terminplaner: Einstellungen und Termine kosten nichts und sind keine Erklaerungen im Namen des Kunden (Spec 2026-10-02, // Abschnitt 9). Genau diese sechs Pfade, jeder einzeln (keine Wildcard, Spec 2026-10-03): alles andere unter /api/termine, // auch der Fragebogen (PUT /fragebogen, Bestellung), ist in der Kundenansicht gesperrt, ohne dass es einen Eintrag braucht. ['POST', /^\/api\/termine\/einstellungen\/vorlage\/?$/], ['POST', /^\/api\/termine\/ressourcen\/[^/]+\/feed\/?$/], ['POST', /^\/api\/termine\/buchungen\/?$/], ['POST', /^\/api\/termine\/buchungen\/[^/]+\/aktion\/?$/], ['PATCH', /^\/api\/termine\/einstellungen\/?$/], ['DELETE', /^\/api\/termine\/buchungen\/[^/]+\/?$/], // Webshop: Artikel, Einstellungen, Rechtstexte-Entwurf und Bilder darf das Team mit dem Kunden ausfuellen (protokolliert). Genau diese // sieben Pfade, jeder einzeln: Anfrage, Oeffnen, Bestaetigen der Rechtstexte (der Haendler bestaetigt persoenlich), Stripe und // Erstattungen bleiben gesperrt, ohne dass es einen Eintrag braucht. ['POST', /^\/api\/shop\/produkte\/?$/], ['POST', /^\/api\/shop\/produkte\/sortierung\/?$/], ['PUT', /^\/api\/shop\/produkte\/[^/]+\/?$/], ['DELETE', /^\/api\/shop\/produkte\/[^/]+\/?$/], ['PUT', /^\/api\/shop\/einstellungen\/?$/], ['PUT', /^\/api\/shop\/rechtstexte\/?$/], ['POST', /^\/api\/shop\/bild\/?$/], // Oeffnungszeiten: Speichern im Namen des Kunden (Spec 2026-10-04, Abschnitt 3.4); den WhatsApp-Haken sperrt die Route selbst (403). // Nur dieser eine Pfad: die Vorschau (GET) braucht keinen Eintrag, alles andere unter /api/zeiten ist gesperrt. ['PUT', /^\/api\/zeiten\/[^/]+\/?$/], ['POST', /^\/api\/auth\/kundenansicht\/ende\/?$/], ['POST', /^\/api\/auth\/logout\/?$/], ['POST', /^\/api\/auth\/login\/?$/], ] // Eigene Pruefung (Ticketsystem-JWT) oder Ablauf/Wechsel selbst behandelt (me, Einloesen); oeffentliche Schnittstellen // (/api/public/: Pinnwand, Terminplaner fuer Gaeste) kennen keine Sitzung und ignorieren ein Kundenansicht-Cookie im Browser const PASS_PREFIXES = ['/api/team/', '/api/admin/', '/api/public/'] const PASS_EXACT = ['/api/auth/me', '/api/auth/kundenansicht'] const READ_METHODS = ['GET', 'HEAD', 'OPTIONS'] const replaced = (res, session) => res.status(409).json({ code: 'view_replaced', kunde: session.name || '', error: 'Diese Ansicht wurde durch eine andere Kundenansicht ersetzt.' }) export async function kundenansichtGuard(req, res, next) { // Express routet ohne Ruecksicht auf Gross-/Kleinschreibung (/API/news trifft /api/news): hier genauso pruefen const pathname = String(req.path || '').toLowerCase() if (!pathname.startsWith('/api/')) return next() if (PASS_PREFIXES.some((p) => pathname.startsWith(p)) || PASS_EXACT.includes(pathname.replace(/\/$/, ''))) return next() const session = parsePortalSession(req) if (!session) return next() const current = isKundenansicht(session) // Abgelaufen oder alte Ansicht ohne Kennung (frueheres Admin-Panel): beenden, nichts mehr lesen oder schreiben if (session.viewAs && (!current || isExpired(session))) { endKundenansicht(res) if (current) await logOnce(session, 'abgelaufen') return res.status(401).json({ code: 'view_expired', error: VIEW_EXPIRED_MESSAGE }) } // Tab einer Kundenansicht, deren Cookie inzwischen zu etwas anderem gehoert (andere Kundenansicht, // Kunden-Login): keine fremden Daten in diesen Tab const header = req.get('X-Kundenansicht') if (header && header !== session.viewId) return replaced(res, session) if (!session.viewAs) return next() if (READ_METHODS.includes(req.method)) return next() if (!header) return replaced(res, session) if (KUNDENANSICHT_ALLOWED.some(([method, re]) => method === req.method && re.test(pathname))) return next() return res.status(403).json({ code: 'view_blocked', error: 'In der Kundenansicht nicht möglich.' }) }