import { Router } from 'express' import { getSessionCustomerId, requireSession } from '../middleware/session.js' import { assertOwnProject, getFeatureDoc } from '../services/siteFeatures.js' import { getDocument } from '../services/appwriteAdmin.js' import { config } from '../config.js' import { isKundenansicht } from '../services/kundenansicht.js' import { createWriteLimiter } from '../services/writeLimiter.js' import { anfrageAnsicht, stelleAnfrage, zieheZurueck } from '../services/shop/anfrage.js' import { shopFuerKunde } from '../services/shop/schalter.js' /** * Kunden-Schnittstelle des Webshops: /api/shop (Spezifikation 2026-10-05-webshop-design.md). Dieser Stand (Task B1) enthaelt * die Anfrage: GET /status, POST /anfrage, POST /anfrage/zurueckziehen. Weitere Routen kommen mit C7, D2 und E5. * * Jede Route: Session, projectId (Query oder Body), eigenes Projekt. Schreib-Routen sind in der Kundenansicht gesperrt * (kundenansichtGuard: nicht auf der Erlaubnisliste; hier zusaetzlich 403) und zaehlen gegen 20 Aufrufe je Kunde und Stunde. * Fehler: { error: , fehler?: { : Text }, code? }; Texte von Appwrite oder unbekannten Fehlern verlassen den Server nie. */ const router = Router() const MAX_WRITES_PER_HOUR = 20 const MELDUNG = { projekt: 'Bitte wähle ein Projekt.', fremd: 'Kein Zugriff auf dieses Projekt.', kundenansicht: 'In der Kundenansicht kann das nicht geändert werden.', zuViele: 'Zu viele Änderungen in kurzer Zeit. Bitte versuch es in einer Stunde wieder.', } /** Fehler des Kunden (4xx/503 ohne Appwrite-Typ) wie sie sind, alles andere 500 ohne Rohtext. */ function fail(res, err) { const own = ((err?.status >= 400 && err.status < 500) || err?.status === 503) && !err.type if (!own) { console.error(`[shop] ${err?.name ?? 'Fehler'} ${err?.status ?? ''} ${String(err?.message ?? '').slice(0, 200)}`.trim()) return res.status(500).json({ error: 'Interner Serverfehler' }) } const body = { ...err.body, error: err.message } if (err.fehler && typeof err.fehler === 'object') body.fehler = err.fehler return res.status(err.status).json(body) } const route = (fn) => async (req, res, next) => { try { await fn(req, res, next) } catch (err) { fail(res, err) } } const limiter = createWriteLimiter({ max: MAX_WRITES_PER_HOUR }) const limitWrites = limiter.middleware((req) => getSessionCustomerId(req), MELDUNG.zuViele) export function resetShopRoutesForTests() { limiter.reset() } const nurKunde = (req, res, next) => { if (isKundenansicht(req.session) || req.session?.viewAs) return res.status(403).json({ error: MELDUNG.kundenansicht }) return next() } const person = (req) => ({ name: req.session?.name || '', email: req.session?.email || '' }) const projectIdOf = (req) => { const raw = req.query.projectId ?? req.body?.projectId return typeof raw === 'string' ? raw : '' } router.use((_req, res, next) => { res.set('Cache-Control', 'no-store') next() }) /** * Stand des Webshops fuer ein Projekt: { verfuegbar, state, anfrage }. verfuegbar = Schalter fuer diesen Kunden; * state = Zustand der Funktion (off, wenn es noch kein Dokument gibt); anfrage = anfrageAnsicht oder null. Legt nichts an. */ router.get('/status', requireSession, route(async (req, res) => { const projectId = projectIdOf(req) if (!projectId) throw Object.assign(new Error(MELDUNG.projekt), { status: 400 }) const customerId = getSessionCustomerId(req) try { await assertOwnProject(customerId, projectId) } catch (err) { throw err?.status === 403 || err?.status === 400 ? Object.assign(new Error(MELDUNG.fremd), { status: 403 }) : err } const verfuegbar = shopFuerKunde(req.session?.email) const feature = await getFeatureDoc(customerId, projectId, 'shop') let shop = null if (feature) { try { shop = await getDocument(config.collections.shopConfig, feature.$id) } catch (err) { if (!(err?.status === 404 && err.type !== 'collection_not_found' && err.type !== 'database_not_found')) throw err } } res.json({ verfuegbar, state: feature?.state || 'off', anfrage: shop ? anfrageAnsicht(shop) : null }) })) router.post('/anfrage', requireSession, nurKunde, limitWrites, route(async (req, res) => { const anfrage = await stelleAnfrage({ customerId: getSessionCustomerId(req), projectId: projectIdOf(req), eingabe: req.body?.anfrage, person: person(req), }) res.json({ anfrage }) })) router.post('/anfrage/zurueckziehen', requireSession, nurKunde, limitWrites, route(async (req, res) => { const anfrage = await zieheZurueck({ customerId: getSessionCustomerId(req), projectId: projectIdOf(req), person: person(req) }) res.json({ anfrage }) })) export default router