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 <noreply@anthropic.com>
This commit is contained in:
ClaudeandClaude Sonnet 5.5 committed 2026-10-07 15:20:24 +00:00
1 parent c2245b6905
commit b6c1d81342
3 files changed
+32 -4

No files matched your search

+2 -2
View File
@@ -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.'
@@ -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 = {
+21 -2
View File
@@ -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')