From 45487d6c7ef53c284e345f0a6333fb42ce9ce127 Mon Sep 17 00:00:00 2001 From: Claude Date: Mon, 5 Oct 2026 09:38:16 +0000 Subject: [PATCH] Shop Review A M3: Eingangsbestaetigung G12 je Empfaengeradresse begrenzen (3 je Stunde) Ueber die gemeinsame Empfaenger-Kappe (darfEmpfaenger) geht an dieselbe Adresse hoechstens dreimal je Stunde ein G12. Darueber hinaus wird der Widerruf trotzdem gespeichert und mit eingang beantwortet, es wird aber keine weitere Mail eingereiht (bestaetigungAt bleibt leer). Co-Authored-By: Claude Opus 5.5 --- server/services/shop/widerrufe.js | 6 ++++-- test/shopWiderrufe.test.js | 25 +++++++++++++++++++++++++ 2 files changed, 29 insertions(+), 2 deletions(-) diff --git a/server/services/shop/widerrufe.js b/server/services/shop/widerrufe.js index 6034a65..263dbc1 100644 --- a/server/services/shop/widerrufe.js +++ b/server/services/shop/widerrufe.js @@ -1,7 +1,7 @@ import { config } from '../../config.js' import { createDocument, getDocument, listDocuments, Query, updateDocument } from '../appwriteAdmin.js' import { withLocks } from '../keyedLock.js' -import { darfWiderrufsversuch, zaehle } from '../termine/limits.js' +import { darfEmpfaenger, darfWiderrufsversuch, zaehle } from '../termine/limits.js' import { pruefeNow } from '../termine/util.js' import { datumKurz, uhrzeit } from '../termine/zeit.js' import { bestellDaten } from './bestellungen.js' @@ -147,8 +147,10 @@ export async function erfasseWiderruf(ctx, eingabe, { now = new Date(), ip } = { if (order) await verlaufErgaenzen(order.$id, now) // G12: Eingangsbestaetigung auf dauerhaftem Datentraeger, sofort; bestaetigungAt nur, wenn die Warteschlange sie angenommen hat + // Pro Empfaengeradresse hoechstens 3 je Stunde (gemeinsame Kappe der Funktionen): sonst liesse sich G12 als Mail-Relay an fremde + // Adressen missbrauchen. Der Widerruf ist gespeichert und beantwortet, die Bestaetigung entfaellt dann (bestaetigungAt bleibt leer). try { - const g12 = await anKaeufer('G12', ctx, order, { an: email, bookingId: doc.$id, name, bestellnummer, eingang: now, nachricht }, now) + const g12 = !darfEmpfaenger(email.trim().toLowerCase(), now) ? null : await anKaeufer('G12', ctx, order, { an: email, bookingId: doc.$id, name, bestellnummer, eingang: now, nachricht }, now) if (g12?.ok) await updateDocument(col(), doc.$id, { bestaetigungAt: now.toISOString() }) } catch (err) { console.warn(`[shop-widerrufe] G12 nicht bestätigt: ${err?.name}`) diff --git a/test/shopWiderrufe.test.js b/test/shopWiderrufe.test.js index 79d78cf..1498e31 100644 --- a/test/shopWiderrufe.test.js +++ b/test/shopWiderrufe.test.js @@ -94,6 +94,11 @@ afterEach(async () => { const widerrufe = () => [...aw.docs('shopWiderrufe').values()] const outbox = () => [...aw.docs('termineOutbox').values()] +/** Zahl der G12 in der Warteschlange zu Widerrufen mit dieser E-Mail-Adresse (ohne passende Bestellung gilt die Widerruf-ID als bookingId) */ +const g12An = (email) => { + const ids = new Set(widerrufe().filter((d) => d.email === email).map((d) => d.$id)) + return outbox().filter((e) => e.art === 'G12' && ids.has(e.bookingId)).length +} const fehlerVon = async (promise) => { try { await promise @@ -257,6 +262,26 @@ test('Grenzen: 10 je IP und Stunde ⇒ 429, danach wieder frei; 20 je Shop und S await erfasseWiderruf(ctxMit('active', 'f_shop02'), EINGABE, { now: t, ip: '198.51.100.99' }) }) +test('M3: G12 geht je Empfängeradresse höchstens 3 Mal pro Stunde raus; der Widerruf wird trotzdem gespeichert und mit eingang beantwortet', async () => { + const ziel = { ...EINGABE, email: 'opfer@example.com' } + const antworten = [] + for (let i = 0; i < 5; i += 1) antworten.push(await erfasseWiderruf(ctxMit(), ziel, { now: nach(i * 1000), ip: `203.0.113.${100 + i}` })) + assert.ok(antworten.every((a) => a.ok === true && typeof a.eingang === 'string' && a.eingang !== '')) + assert.equal(widerrufe().filter((d) => d.email === 'opfer@example.com').length, 5, 'alle Widerrufe gespeichert') + await warteAufVersand() + assert.equal(g12An('opfer@example.com'), 3) + const mitBestaetigung = widerrufe().filter((d) => d.email === 'opfer@example.com' && d.bestaetigungAt).length + assert.equal(mitBestaetigung, 3) + // eine andere Adresse ist nicht betroffen + await erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'andere@example.com' }, { now: nach(6000), ip: '203.0.113.120' }) + await warteAufVersand() + assert.equal(g12An('andere@example.com'), 1) + // nach einer Stunde geht wieder eine Bestaetigung raus + await erfasseWiderruf(ctxMit(), ziel, { now: nach(61 * 60 * 1000), ip: '203.0.113.130' }) + await warteAufVersand() + assert.equal(g12An('opfer@example.com'), 4) +}) + test('ein ungültiger Versuch zählt gegen die Grenze (kein Raten ohne Limit)', async () => { const ip = '203.0.113.9' for (let i = 0; i < 10; i += 1) await fehlerVon(erfasseWiderruf(ctxMit(), { ...EINGABE, email: 'x' }, { now: JETZT, ip }))