From 2bc2cfac6fd521e3296806de8daa5c820f5cf533 Mon Sep 17 00:00:00 2001 From: Matt Date: Thu, 25 Jun 2026 14:45:34 +0200 Subject: [PATCH] fix(eoi): render signed-PDF preview inline (preview endpoint + font-src) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit After adding frame-src the preview iframe stopped showing the broken-file icon but went blank: it pointed at /api/v1/files/[id]/download, which presigns with the filename so S3 returns Content-Disposition: attachment — the browser downloaded the PDF instead of rendering it. Point the SignedPdfPreview iframe at the existing /preview endpoint, which presigns WITHOUT a filename (inline disposition) so the native PDF viewer renders. Also widen font-src to include https: so react-pdf/pdf.js can load its standard-font pack (LiberationSans*) — previously blocked by font-src 'self' data:, breaking the pdf.js-based viewers' glyphs. Co-Authored-By: Claude Opus 4.8 (1M context) --- next.config.ts | 3 ++- src/components/interests/interest-eoi-tab.tsx | 11 ++++++++--- src/proxy.ts | 4 +++- 3 files changed, 13 insertions(+), 5 deletions(-) diff --git a/next.config.ts b/next.config.ts index 7f85c105..f755c43b 100644 --- a/next.config.ts +++ b/next.config.ts @@ -51,7 +51,8 @@ const csp = [ `script-src 'self' 'unsafe-inline'${isProd ? '' : " 'unsafe-eval'"}${devScriptHosts}`, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https:", - "font-src 'self' data:", + // https: so react-pdf/pdf.js can load its standard-font pack + branding fonts. + "font-src 'self' data: https:", `connect-src 'self' ws: wss: https:${devConnectHosts}`, // PDF previews iframe a presigned storage URL; embedded-signing iframes the // Documenso host. Both are per-port/per-env, so allow https: (matching diff --git a/src/components/interests/interest-eoi-tab.tsx b/src/components/interests/interest-eoi-tab.tsx index 249308b1..78416fb1 100644 --- a/src/components/interests/interest-eoi-tab.tsx +++ b/src/components/interests/interest-eoi-tab.tsx @@ -868,10 +868,15 @@ function SignedEoiCard({ * the file in a new tab via the alongside View button for full-screen. */ function SignedPdfPreview({ fileId }: { fileId: string }) { - const { data, isLoading, isError } = useQuery<{ data: { url: string; filename: string } }>({ - queryKey: ['files', fileId, 'download-url'], + // Use the PREVIEW endpoint, not /download: /download presigns with the + // filename so S3 returns `Content-Disposition: attachment`, which makes the + // iframe trigger a file download (blank preview) instead of rendering. The + // preview endpoint presigns WITHOUT a filename → inline disposition → the + // browser's native PDF viewer renders it in the card. + const { data, isLoading, isError } = useQuery<{ data: { url: string; mimeType: string } }>({ + queryKey: ['files', fileId, 'preview-url'], queryFn: () => - apiFetch<{ data: { url: string; filename: string } }>(`/api/v1/files/${fileId}/download`), + apiFetch<{ data: { url: string; mimeType: string } }>(`/api/v1/files/${fileId}/preview`), // Presigned URL TTLs vary per backend - refresh well before they // expire so a long-open card doesn't suddenly 403. 4 minutes is // comfortably below the 5-minute MinIO default. diff --git a/src/proxy.ts b/src/proxy.ts index 4886aed1..8b777016 100644 --- a/src/proxy.ts +++ b/src/proxy.ts @@ -27,7 +27,9 @@ function buildCspWithNonce(nonce: string, isProd: boolean): string { scriptSrc, "style-src 'self' 'unsafe-inline'", "img-src 'self' data: blob: https:", - "font-src 'self' data:", + // https: so react-pdf/pdf.js can pull its standard-font pack (the PDF + // viewers fetch LiberationSans etc. from a CDN) and port-branding fonts. + "font-src 'self' data: https:", connectSrc, // PDF previews (signed EOIs etc.) iframe a presigned storage URL, and the // embedded-signing card iframes the Documenso host. Both are per-port /