Terminplaner: oeffentliche Schnittstelle v1 mit Missbrauchsschutz
Router /api/public/termine/v1 (config, tage, zeiten, buchungen, widerruf, Token-Seiten /termin/*, Kalender-Abo) mit eigenem 16-KB-Parser und Fehlermiddleware, CORS nach Herkunft, Honeypot und Formular-Token, Grenzen je IP (IPv6 je /64), siteKey und Adresse in limits.js, Mindestantwortzeit (TERMINE_MIN_ANTWORT_MS) und 10-Sekunden-Cache fuer tage/zeiten. Vor dem globalen express.json eingehaengt; /api/public/ ist vom Kundenansicht-Waechter ausgenommen. Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
43ac05aa06
commit
2b0c05b8e9
10 files changed
+1928
-7
No files matched your search
@@ -37,7 +37,9 @@ const MOUNTS = [
|
||||
['/api/abo', (await import('../server/routes/abo.js')).default],
|
||||
['/api/funktionen', (await import('../server/routes/funktionen.js')).default],
|
||||
]
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, ...Object.fromEntries(MOUNTS) })
|
||||
// Oeffentliche Schnittstelle (ohne Session) wie in server/index.js hinter dem Waechter; nicht in MOUNTS, weil sie keine Kunden-Route ist
|
||||
const oeffentlich = (await import('../server/routes/publicTermine.js')).default
|
||||
const app = await startTestApp({ '/': kundenansichtGuard, ...Object.fromEntries(MOUNTS), '/api/public/termine/v1': oeffentlich })
|
||||
after(() => Promise.all([app.close(), aw.close(), st.close()]))
|
||||
|
||||
seedFixtures(aw)
|
||||
@@ -210,3 +212,29 @@ test('Tab mit Kennung, Cookie ist keine Kundenansicht mehr -> 409 statt fremder
|
||||
// ohne Cookie: kein fremder Kunde, die Route antwortet selbst mit 401
|
||||
assert.equal((await call('GET', '/api/projects', { headers: header })).status, 401)
|
||||
})
|
||||
|
||||
// Terminplaner (Aufgabe 16): Gaeste buchen ohne Sitzung; ein Kundenansicht-Cookie im selben Browser (Mitarbeiter testet die
|
||||
// Website des Kunden) darf ihre POSTs weder sperren noch beenden
|
||||
test('/api/public/ ist ausgenommen: Kundenansicht-Cookie sperrt oeffentliche POSTs nicht und beendet keine Ansicht', async (t) => {
|
||||
const vorher = config.termine.minAntwortMs
|
||||
config.termine.minAntwortMs = 0 // sonst wartet jede Buchungsantwort 300 ms
|
||||
t.after(() => (config.termine.minAntwortMs = vorher))
|
||||
const url = '/api/public/termine/v1/abcdefghijklmnop/buchungen'
|
||||
const faelle = [
|
||||
['Kundenansicht mit Kennung', kv()],
|
||||
['Kundenansicht ohne Kennung (sonst 409)', { cookie: kundenansichtCookie() }],
|
||||
['fremde Kennung (sonst 409)', { cookie: kundenansichtCookie(), headers: { 'x-kundenansicht': 'b'.repeat(32) } }],
|
||||
['abgelaufene Ansicht (sonst 401)', kv({ viewExpiresAt: Date.now() - 1000 })],
|
||||
['alte Ansicht ohne viewId (sonst 401)', { cookie: viewAsCookie('c1') }],
|
||||
]
|
||||
for (const [was, opts] of faelle) {
|
||||
for (const pfad of [url, url.replace('/api/public/', '/API/Public/')]) {
|
||||
const res = await call('POST', pfad, { ...opts, body: {} })
|
||||
assert.equal(res.status, 200, `${was} ${pfad}`)
|
||||
assert.deepEqual(res.body, { aktiv: false }, `${was} ${pfad}`) // Modus off: die Schnittstelle antwortet selbst
|
||||
assert.equal(clears(res, config.cookieName), false, `${was}: das Portal-Cookie bleibt`)
|
||||
}
|
||||
}
|
||||
// ausserhalb von /api/public/ bleibt die Sperre
|
||||
assert.equal((await call('POST', '/api/chat/read', kv())).status, 403)
|
||||
})
|
||||
Reference in new issue
Block a user