feat(accounting): rasterise inbound PDFs server-side (never serve raw to browser)
Security hardening for inbound supplier-invoice previews. The admin UI no
longer renders raw PDFs — a malicious inbound PDF could otherwise run embedded
JS or phone home in the admin's session. Instead PDFs are rasterised to flat
PNGs server-side and only those images are shown.
- backend: new rasterizeService shells out to poppler `pdftoppm` (added to the
Docker image via apk poppler-utils — an OS package, NOT a Node PDF lib, so it
respects the pdfkit+pdf-lib "no third PDF lib" rule). pdftoppm executes no JS
and fetches no remote resources, so it doubles as the SSRF/phone-home guard.
Rendered pages cached under storage/business-docs/inbound/rendered/<id>/.
- GET /inbound/:id/page/:n streams the rasterised PNG (CSP default-src 'none'
+ nosniff). GET /inbound/:id/file now serves PDFs as a DOWNLOAD only
(Content-Disposition: attachment) — never inline; images still inline.
- frontend: triage preview switched from a raw-PDF <iframe> to rasterised page
images (getInboundPageBlob), defaulting to the LAST page (QR-bill) with
prev/next nav for multi-page PDFs; images stream as before.
- i18n: previewError / prevPage / nextPage / pageOf (EN + DE).
REQUIRES A BACKEND IMAGE REBUILD (Dockerfile adds poppler-utils) — a plain
`docker compose pull` of a stale image won't have pdftoppm; the route then
returns 503 RASTERIZER_UNAVAILABLE and the UI shows "preview unavailable".
Verified: node -c, a pdfkit->pdftoppm rasterise smoke test (renders + caches),
en/de JSON valid, npm run build green.
This commit is contained in:
@@ -3430,7 +3430,11 @@
|
||||
"noAmount": "Betrag nicht erfasst",
|
||||
"rebillHint": "Erstellt eine bearbeitbare geplante Rechnung beim Kunden. MwSt-/Steuerbehandlung ist v1 — mit Treuhänder prüfen.",
|
||||
"previewLoading": "Vorschau wird geladen…",
|
||||
"qrHint": "An der letzten Seite geöffnet — der Schweizer QR-Einzahlschein sitzt meist unten.",
|
||||
"previewError": "Vorschau nicht verfügbar — Felder manuell erfassen.",
|
||||
"qrHint": "Letzte Seite — der Schweizer QR-Einzahlschein sitzt meist unten.",
|
||||
"prevPage": "Zurück",
|
||||
"nextPage": "Weiter",
|
||||
"pageOf": "Seite {{n}} / {{total}}",
|
||||
"status": {
|
||||
"unsorted": "Neu",
|
||||
"categorized": "Kategorisiert",
|
||||
|
||||
@@ -3430,7 +3430,11 @@
|
||||
"noAmount": "amount not entered",
|
||||
"rebillHint": "Creates an editable scheduled invoice on the client. VAT/tax handling is v1 — verify with your Treuhänder.",
|
||||
"previewLoading": "Loading preview…",
|
||||
"qrHint": "Opened at the last page — the Swiss QR-bill usually sits at the bottom.",
|
||||
"previewError": "Preview unavailable — enter the fields manually.",
|
||||
"qrHint": "Showing the last page — the Swiss QR-bill usually sits at the bottom.",
|
||||
"prevPage": "Prev",
|
||||
"nextPage": "Next",
|
||||
"pageOf": "Page {{n}} / {{total}}",
|
||||
"status": {
|
||||
"unsorted": "New",
|
||||
"categorized": "Categorized",
|
||||
|
||||
Reference in New Issue
Block a user