Files
Webklar-Kundenbereich/server/routes/auftrag.js
T
ClaudeandClaude Sonnet 5.5 b6c1d81342 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>
2026-10-07 15:20:24 +00:00

119 lines
4.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* Öffentliche Link-Routen einer Auftragsbestätigung (Übersicht V5, Strang L5): ansehen, PDF, bestätigen – ohne
* Anmeldung, das Token steht im Link (Fragment `#t=`, die Seite schickt es hierher). Mount: /api/auftrag.
* Ratenbegrenzung nur im Arbeitsspeicher (GET 120/h, POST 10/h je IP); nichts von der Anfrage wird gespeichert oder
* protokolliert. Ungültige, unbekannte, abgelaufene Links und abgeschalteter Schalter antworten gleich (404).
*/
import { Router } from 'express'
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, 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 = 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.'
const echteUhr = () => new Date()
let uhr = echteUhr
const jetztMs = () => uhr().getTime()
const getLimiter = createWriteLimiter({ max: 120, now: jetztMs })
const postLimiter = createWriteLimiter({ max: 10, now: jetztMs })
const nachIp = (req) => String(req.ip)
/** Nur für Tests: feste Uhr der Routen (`null` = echte Uhr). */
export function setzeUhrFuerTests(fn) {
uhr = typeof fn === 'function' ? fn : echteUhr
}
/** Nur für Tests: Zähler leeren und echte Uhr. */
export function resetAuftragRoutesForTests() {
getLimiter.reset()
postLimiter.reset()
uhr = echteUhr
}
const router = Router()
router.use((_req, res, next) => {
res.set({
'Cache-Control': 'no-store',
'X-Robots-Tag': 'noindex',
'X-Content-Type-Options': 'nosniff',
'Referrer-Policy': 'no-referrer',
})
next()
})
/** Gemeinsamer Vorlauf der Link-Routen; bei `null` ist die Antwort (404/410) schon gesendet. */
async function ladeNachToken(req, res) {
const nichtGueltig = () => {
res.status(404).json({ error: UNGUELTIG })
return null
}
const hash = hashToken(req.params.token)
if (hash === null) return nichtGueltig()
const a = await findAuftragByTokenHash(hash)
if (!a) return nichtGueltig()
if (!auftraegeAktiv({ email: a.kunde?.email || a.gesendetAn })) return nichtGueltig()
if (a.status === 'entwurf') return nichtGueltig()
if (GESPERRT_MELDUNG[a.status]) {
res.status(410).json({ error: GESPERRT_MELDUNG[a.status] })
return null
}
const bis = a.tokenGueltigBis ? Date.parse(a.tokenGueltigBis) : Number.NaN
if (!Number.isFinite(bis) || jetztMs() > bis) return nichtGueltig()
return a
}
/** AB-PDF aus der Ablage senden (inline); fehlt die Datei, 404 `PDF nicht gefunden.`. */
export async function sendePdf(res, auftrag) {
let buffer
try {
buffer = await lesePdf(auftrag.pdfDatei)
} catch (err) {
if (err instanceof AuftragError) throw err
console.error(`[auftraege] PDF zu ${auftrag.nummer} nicht lesbar: ${err.code || err.message}`)
throw new AuftragError(404, 'PDF nicht gefunden.')
}
const nummer = String(auftrag.nummer || 'Auftragsbestaetigung').replace(/[^A-Za-z0-9-]/g, '') || 'Auftragsbestaetigung'
res.writeHead(200, { 'Content-Type': 'application/pdf', 'Content-Disposition': `inline; filename="${nummer}.pdf"` })
res.end(buffer)
}
router.get('/:token', getLimiter.middleware(nachIp, ZU_VIELE), async (req, res) => {
try {
const a = await ladeNachToken(req, res)
if (a) res.json({ auftrag: oeffentlicheAnsicht(a) })
} catch (err) {
sendeFehler(res, 'auftrag', err)
}
})
router.get('/:token/pdf', getLimiter.middleware(nachIp, ZU_VIELE), async (req, res) => {
try {
const a = await ladeNachToken(req, res)
if (a) await sendePdf(res, a)
} catch (err) {
sendeFehler(res, 'auftrag', err)
}
})
router.post('/:token/bestaetigen', postLimiter.middleware(nachIp, ZU_VIELE), async (req, res) => {
try {
const a = await ladeNachToken(req, res)
if (!a) return
const body = req.body && typeof req.body === 'object' ? req.body : {}
if (body.bestaetigt !== true) return res.status(400).json({ error: HAEKCHEN })
const neu = await bestaetigeAuftrag(a, { ueber: 'link', fruehbeginn: body.fruehbeginn === true, jetzt: uhr() })
res.json({ auftrag: oeffentlicheAnsicht(neu) })
} catch (err) {
sendeFehler(res, 'auftrag', err)
}
})
export default router