From b6c1d813424d2de86412946c5f068f5a03f3f030 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 7 Oct 2026 15:20:24 +0000 Subject: [PATCH] Kontrolle: Bestaetigen per Link prueft Token und Ablauf in der Sperre erneut Ein Token-Tausch durch die Erinnerung zwischen Pruefung und Sperre ergibt 404 wie bei einem ungueltigen Link; das Portal ist nicht betroffen. Co-Authored-By: Claude Sonnet 5.5 --- server/routes/auftrag.js | 4 ++-- server/services/auftraege/bestaetigung.js | 9 +++++++++ test/auftraegeBestaetigung.test.js | 23 +++++++++++++++++++++-- 3 files changed, 32 insertions(+), 4 deletions(-) diff --git a/server/routes/auftrag.js b/server/routes/auftrag.js index 033fa39..f120242 100644 --- a/server/routes/auftrag.js +++ b/server/routes/auftrag.js @@ -9,12 +9,12 @@ import { createWriteLimiter } from '../services/writeLimiter.js' import { findAuftragByTokenHash } from '../services/auftraege/auftragStore.js' import { lesePdf } from '../services/auftraege/ablage.js' import { oeffentlicheAnsicht } from '../services/auftraege/auftragSicht.js' -import { bestaetigeAuftrag, GESPERRT_MELDUNG } from '../services/auftraege/bestaetigung.js' +import { bestaetigeAuftrag, GESPERRT_MELDUNG, UNGUELTIGER_LINK } from '../services/auftraege/bestaetigung.js' import { AuftragError, sendeFehler } from '../services/auftraege/fehler.js' import { auftraegeAktiv } from '../services/auftraege/schalter.js' import { hashToken } from '../services/auftraege/token.js' -const UNGUELTIG = 'Dieser Link ist ungültig oder abgelaufen.' +const UNGUELTIG = UNGUELTIGER_LINK const HAEKCHEN = 'Bitte bestätigen Sie den Auftrag mit dem Häkchen.' const ZU_VIELE = 'Zu viele Anfragen. Bitte später erneut versuchen.' diff --git a/server/services/auftraege/bestaetigung.js b/server/services/auftraege/bestaetigung.js index c33eedf..efd8089 100644 --- a/server/services/auftraege/bestaetigung.js +++ b/server/services/auftraege/bestaetigung.js @@ -16,6 +16,7 @@ export const GESPERRT_MELDUNG = { storniert: 'Diese Auftragsbestätigung wurde storniert.', widerrufen: 'Diese Auftragsbestätigung wurde widerrufen.', } +export const UNGUELTIGER_LINK = 'Dieser Link ist ungültig oder abgelaufen.' const NICHT_BESTAETIGBAR = 'Diese Auftragsbestätigung kann nicht bestätigt werden.' const NICHT_GEFUNDEN = 'Auftragsbestätigung nicht gefunden.' const GESPERRT_STATUS = ['entwurf', 'ersetzt', 'storniert', 'widerrufen'] @@ -39,6 +40,14 @@ export async function bestaetigeAuftrag(auftrag, { ueber, fruehbeginn, jetzt = n if (!frisch) throw new AuftragError(404, NICHT_GEFUNDEN) const lage = bestaetigungsLage(frisch) if (lage === 'gesperrt') throw new AuftragError(409, GESPERRT_MELDUNG[frisch.status] ?? NICHT_BESTAETIGBAR) + if (ueber === 'link') { + // Die Route hat Token und Ablauf auf einem Stand ausserhalb der Sperre geprueft; eine Erinnerung (Token-Tausch) kann + // dazwischen gelaufen sein: dann gilt der alte Link nicht mehr, wie bei jedem ungueltigen Link (404) + const bis = frisch.tokenGueltigBis ? Date.parse(frisch.tokenGueltigBis) : Number.NaN + if (!frisch.tokenHash || frisch.tokenHash !== auftrag.tokenHash || !Number.isFinite(bis) || jetzt.getTime() > bis) { + throw new AuftragError(404, UNGUELTIGER_LINK) + } + } if (lage === 'bestaetigt') return { aktuell: frisch, bestaetigtJetzt: false } const privat = frisch.kunde?.kundenart === 'privat' const patch = { diff --git a/test/auftraegeBestaetigung.test.js b/test/auftraegeBestaetigung.test.js index 778753f..e081f21 100644 --- a/test/auftraegeBestaetigung.test.js +++ b/test/auftraegeBestaetigung.test.js @@ -3,13 +3,13 @@ import assert from 'node:assert/strict' import { startAppwriteMock } from './support/appwriteMock.js' import { useTestEnv } from './support/testEnv.js' import { mailSink } from './support/mailSink.js' -import { AUFTRAEGE_DATETIMES, AUFTRAEGE_SCHEMA, abDokument, kunde } from './auftraegeBestaetigungHilfe.js' +import { AUFTRAEGE_DATETIMES, AUFTRAEGE_SCHEMA, UNGUELTIG, TOKEN_NEU, abDokument, kunde } from './auftraegeBestaetigungHilfe.js' // bestaetigung.js: Bestätigen (idempotent, unter Sperre) und Widerrufen einer AB; Mails über die Senke const aw = await startAppwriteMock({ schema: AUFTRAEGE_SCHEMA, datetimes: AUFTRAEGE_DATETIMES, defaultLimit: 25 }) useTestEnv({ appwriteUrl: aw.url, stripeUrl: 'http://127.0.0.1:1' }) const { config } = await import('../server/config.js') -const { auftragToDoc, getAuftrag } = await import('../server/services/auftraege/auftragStore.js') +const { auftragToDoc, getAuftrag, updateAuftrag } = await import('../server/services/auftraege/auftragStore.js') const { AuftragError } = await import('../server/services/auftraege/fehler.js') const { FRUEHBEGINN_FASSUNG, FRUEHBEGINN_TEXT } = await import('../server/services/auftraege/texte.js') const { sperreAuftrag } = await import('../server/services/auftraege/konstanten.js') @@ -174,6 +174,25 @@ test('bestaetigeAuftrag: ungültiger Weg oder Zeitpunkt wirft TypeError, unbekan assert.equal(sink.sent.length, 0) }) +test('bestaetigeAuftrag über den Link: ein Token-Tausch (Erinnerung) oder Ablauf zwischen Prüfung und Sperre ergibt 404 wie bei ungültigem Link', async () => { + const alt = seed() + const nichtsGeaendert = async () => { + assert.equal((await getAuftrag('ab1')).status, 'versendet') + assert.equal(sink.sent.length, 0) + } + // die Erinnerung tauscht den Token, nachdem die Route den alten geprüft hat + const { hashToken } = await import('../server/services/auftraege/token.js') + await updateAuftrag('ab1', { tokenHash: hashToken(TOKEN_NEU) }) + await assert.rejects(bestaetigeAuftrag(alt, { ueber: 'link', fruehbeginn: false, jetzt: JETZT }), (err) => err instanceof AuftragError && err.status === 404 && err.message === UNGUELTIG) + await nichtsGeaendert() + // der Link ist inzwischen abgelaufen + await updateAuftrag('ab1', { tokenHash: alt.tokenHash, tokenGueltigBis: '2026-10-07T08:29:59.000Z' }) + await assert.rejects(bestaetigeAuftrag(alt, { ueber: 'link', fruehbeginn: false, jetzt: JETZT }), (err) => err instanceof AuftragError && err.status === 404 && err.message === UNGUELTIG) + await nichtsGeaendert() + // das Portal braucht keinen Link: dort zaehlt der Token nicht + assert.equal((await bestaetigeAuftrag(alt, { ueber: 'portal', fruehbeginn: false, jetzt: JETZT })).status, 'bestaetigt') +}) + // gesendet am 06.10.2026 12:00 Berlin: die Frist endet am 20.10.2026 um 24 Uhr (= 21:59:59,999 UTC) const FRIST_ENDE = new Date('2026-10-20T21:59:59.999Z')