Shop C7: Kundenrouten der Verwaltung (Status, Einstellungen, Artikel, Rechtstexte, Oeffnen, Bilder)

- routes/shop.js: gemeinsame Pruefung mitShop (Schalter, eigenes Projekt, Funktion setup|active, lesend auch disabled, shopConfig
  bei Bedarf anlegen); GET /status erweitert (Bereitschaft, geoeffnet, Snippet, Vorschau, Stripe ohne Konto-ID, Zahlen);
  Einstellungen, Artikel (mit Bild-Pruefung, Beanspruchen und Aufraeumen), Sortierung, Rechtstexte (Entwurf, Bestaetigen mit Hash),
  Oeffnen/Schliessen, Bild-Upload (octet-stream, 8 MB) und Vorschau; Schreib-Limit 300 je Stunde und Kunde
- services/shop/bilder.js: Artikelbilder nach dem Muster von newsImages.js (1600/480 px WebP, Sidecar, 24-h-Aufraeumen, 40 Uploads/h)
- Kundenansicht: sieben Verwaltungs-Pfade erlaubt und als shop_aenderung protokolliert; Anfrage, Oeffnen, Bestaetigen, Stripe bleiben gesperrt
- Tests: shopRoutes, shopBilder; Erwartungen von kundenansichtGuard, customerViewLog und shopAnfrage angepasst

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Opus 5.5 committed 2026-10-05 08:52:53 +00:00
1 parent 9d19a2cbad
commit 9722f0a938
10 files changed
+1389 -32

No files matched your search

+29 -2
View File
@@ -119,7 +119,7 @@ test('Checkout je Funktion ist in der Kundenansicht gesperrt (Schleife trifft /a
}
})
test('Erlaubnisliste: genau News, Terminplaner, Analyse-Link, Ende, Logout und Login', () => {
test('Erlaubnisliste: genau News, Terminplaner, Webshop-Verwaltung, Analyse-Link, Ende, Logout und Login', () => {
const ok = [
['POST', '/api/news'], ['POST', '/api/news/image'], ['PATCH', '/api/news/abc'], ['DELETE', '/api/news/abc'],
['POST', '/api/funktionen/analyse/heatmap-link'], ['POST', '/api/auth/kundenansicht/ende'],
@@ -127,9 +127,11 @@ test('Erlaubnisliste: genau News, Terminplaner, Analyse-Link, Ende, Logout und L
['POST', '/api/termine/buchungen'], ['POST', '/api/termine/buchungen/abc/aktion'], ['POST', '/api/termine/ressourcen/abc/feed'],
['PATCH', '/api/termine/einstellungen'], ['PATCH', '/api/termine/einstellungen/'],
['DELETE', '/api/termine/buchungen/abc'], ['DELETE', '/api/termine/buchungen/abc/'],
['POST', '/api/shop/produkte'], ['POST', '/api/shop/produkte/'], ['POST', '/api/shop/produkte/sortierung'], ['PUT', '/api/shop/produkte/abc'],
['DELETE', '/api/shop/produkte/abc'], ['PUT', '/api/shop/einstellungen'], ['PUT', '/api/shop/rechtstexte'], ['POST', '/api/shop/bild'],
]
for (const [m, p] of ok) assert.ok(allowed(m, p), `${m} ${p}`)
assert.equal(KUNDENANSICHT_ALLOWED.length, 8 + 6, 'genau sechs Termine-Eintraege dazu, keine Wildcard')
assert.equal(KUNDENANSICHT_ALLOWED.length, 8 + 6 + 7, 'genau sechs Termine- und sieben Webshop-Eintraege dazu, keine Wildcard')
for (const [m, p] of [
['POST', '/api/chat/read'], ['POST', '/api/news/abc'], ['POST', '/api/auth/password'], ['POST', '/api/abo/checkout'], ['POST', '/api/funktionen/news/checkout'],
// Terminplaner: nur der eigene Pfad, nur diese Methoden und Formen
@@ -143,6 +145,12 @@ test('Erlaubnisliste: genau News, Terminplaner, Analyse-Link, Ende, Logout und L
['DELETE', '/api/termine/ressourcen/abc'], ['PUT', '/api/termine/buchungen/abc'],
// Webshop: Anfrage stellen und zurueckziehen sind in der Kundenansicht gesperrt (nicht auf der Liste)
['POST', '/api/shop/anfrage'], ['POST', '/api/shop/anfrage/zurueckziehen'],
// Webshop: Oeffnen und Bestaetigen der Rechtstexte macht der Haendler selbst; Stripe und Erstattungen sind nie dabei
['POST', '/api/shop/oeffnen'], ['POST', '/api/shop/rechtstexte/bestaetigen'], ['POST', '/api/shop/stripe/verbinden'], ['POST', '/api/shop/bestellungen/abc/erstatten'],
// und nur die ausgeschriebenen Pfade und Methoden der Verwaltung
['POST', '/api/shop/rechtstexte'], ['POST', '/api/shop/einstellungen'], ['PATCH', '/api/shop/produkte/abc'], ['POST', '/api/shop/produkte/abc'],
['PUT', '/api/shop/produkte'], ['DELETE', '/api/shop/produkte'], ['PUT', '/api/shop/bild'], ['POST', '/api/shop/bild/x'],
['POST', '/api/shopX/produkte'], ['PUT', '/api/shop/produkte/abc/def'], ['DELETE', '/api/shop/einstellungen'],
]) {
assert.ok(!allowed(m, p), `${m} ${p}`)
}
@@ -175,6 +183,25 @@ test('unbekanntes POST/PUT unter /api/termine in der Kundenansicht -> 403 view_b
}
})
// Router-Abgleich Webshop: genau die sieben Verwaltungs-Schreib-Routen stehen auf der Liste, jeder Eintrag trifft genau eine echte Route
test('Router-Abgleich: genau sieben Schreib-Routen des Webshops stehen auf der Erlaubnisliste', () => {
const shopRouter = MOUNTS.find(([prefix]) => prefix === '/api/shop')[1]
const routen = writeRoutes([['/api/shop', shopRouter]])
assert.deepEqual(
routen.filter(({ method, path: p }) => allowed(method, p)).map(({ method, path: p }) => `${method} ${p}`).sort(),
['DELETE /api/shop/produkte/x', 'POST /api/shop/bild', 'POST /api/shop/produkte', 'POST /api/shop/produkte/sortierung',
'PUT /api/shop/einstellungen', 'PUT /api/shop/produkte/x', 'PUT /api/shop/rechtstexte']
)
const shopEintraege = KUNDENANSICHT_ALLOWED.filter(([, re]) => re.source.includes('shop'))
assert.equal(shopEintraege.length, 7)
for (const [m, re] of shopEintraege) assert.equal(routen.filter((r) => r.method === m && re.test(r.path)).length, 1, `${m} ${re}`)
// gesperrt bleiben insbesondere Anfrage, Oeffnen und Bestaetigen
for (const p of ['/api/shop/anfrage', '/api/shop/anfrage/zurueckziehen', '/api/shop/oeffnen', '/api/shop/rechtstexte/bestaetigen']) {
assert.ok(routen.some((r) => r.method === 'POST' && r.path === p), `Route ${p} existiert`)
assert.ok(!allowed('POST', p), p)
}
})
// Router-Abgleich: die Erlaubnisliste und die echten Schreib-Routen des Terminplaners decken sich genau; eine neue Route faellt
// im Test "jede Schreib-Route ausserhalb der Erlaubnisliste -> 403" auf (sie muss gesperrt sein), ein toter Eintrag hier
test('Router-Abgleich: genau sechs Schreib-Routen des Terminplaners stehen auf der Erlaubnisliste', () => {