From fa622cf2f87f447952b2e6e6e9d7fe60a1c14fb7 Mon Sep 17 00:00:00 2001 From: Luca <102960244+Luca-Timo@users.noreply.github.com> Date: Sat, 6 Jun 2026 23:45:54 +0200 Subject: [PATCH] fix(projects): make email preview fully read-only (no clickable links) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inside the preview iframe the Accept button navigated but other links didn't — inconsistent, and worse, clicking Accept/Decline would hit the live action URLs and change the quote state. Sandbox the iframe (no popups/scripts/forms) and force all anchors to target=_blank so every link is inert. Now nothing in the preview is clickable (consistent + safe); scrolling and brand colors are unaffected. --- .../admin/projects/ProjectCockpitPage.tsx | 23 +++++++++++++++---- 1 file changed, 18 insertions(+), 5 deletions(-) diff --git a/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx b/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx index 4875f25e..61da0582 100644 --- a/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx +++ b/frontend/src/pages/admin/projects/ProjectCockpitPage.tsx @@ -83,6 +83,16 @@ const KIND_ICON: Record> = hours: Clock, }; +/** Neutralise links for a read-only preview: force every anchor to target a + * new tab so the sandboxed iframe (no allow-popups) blocks the navigation + * entirely. Without this, clicking "Accept"/"Decline" in the preview would + * hit the live action URLs and actually change the quote's state. */ +function neutralizeLinks(html: string): string { + const base = ''; + if (/]*>/i.test(html)) return html.replace(/]*>/i, (m) => m + base); + return base + html; +} + function minutesToHours(min: number): string { const h = Math.floor(min / 60); const m = min % 60; @@ -445,13 +455,16 @@ export const ProjectCockpitPage: React.FC = () => { {t('projects.email.reRendered', 'Re-rendered from the current template — this email was sent before previews were captured, so it may differ slightly from what the recipient received.')} )} - {/* Render the email exactly as sent — it carries its own - background from the brand/email theme. isolate the - iframe's color-scheme so the admin's OS dark mode doesn't - tint a document that defines its own colors. */} + {/* Read-only preview: renders the email with its own brand + colors (color-scheme:normal stops the dark app theme from + tinting it), but `sandbox` (no allow-popups/scripts/forms) + + neutralizeLinks make every link inert — so the admin + can't accidentally trigger the live Accept/Decline URLs by + clicking inside the preview. Scrolling still works. */}