From 8e64813faabbaf3058a132c4c9316094e29241ff Mon Sep 17 00:00:00 2001 From: Gerard Kavanagh Date: Tue, 18 Aug 2026 19:42:33 +0100 Subject: [PATCH] fix(email): unbreak tenant template writes, vanishing Save header, preview images Three defects from lekkerweed's feedback, plus one sibling found on the way: - Tenant-scope $extends wrapped update/delete wheres of the null-access models (email_templates, email_event_mappings) in AND/OR - an invalid WhereUniqueInput under Prisma 6, so every tenant email-template save, delete and enable/disable toggle threw PrismaClientValidationError -> 500. Writes are now always strictly scoped ({...where, tenantId}); the OR-null widening is reads-only, which also means a tenant write can no longer touch a shared system row. Regression-tested at the exported helpers - the route tests mock @/lib/db wholesale, so the extension never runs there. - EmailEditor/CampaignEditor hardcoded h-[calc(100vh-100px)], overshooting their overflow-hidden hosts by ~120px; caret-reveal scrolling on the first edit pushed the header card - Save button included - out of the clip box with no way to scroll back. Both editors are h-full now, and the two super-admin hosts get the same sized flex-1 overflow-hidden wrapper the tenant pages already had. - The preview pane's srcdoc iframe inherits the admin page's CSP, whose img-src carries no tenant domains - so images the pipeline absolutised against the tenant's own host rendered broken in preview while being perfectly fetchable from a real inbox. Both preview routes now pass req.nextUrl.origin as baseUrlOverride, resolving preview assets (uploads and the shell logo) same-origin with the admin page. Stored and mailed HTML keeps the tenant host. --- .../email-templates/preview/route.ts | 2 + .../email-templates/preview/route.ts | 4 ++ .../app/super-admin/emails/[id]/client.tsx | 55 ++++++++------- .../app/super-admin/emails/new/page.tsx | 47 +++++++------ .../components/admin/email/CampaignEditor.tsx | 5 +- .../components/admin/email/EmailEditor.tsx | 7 +- nextjs_space/lib/db.ts | 27 +++++++- nextjs_space/lib/email/email-preview.ts | 11 +++ .../lib/email/email-render-pipeline.ts | 12 +++- nextjs_space/lib/email/email-shell.ts | 11 ++- .../lib/email/email-template-content.ts | 4 ++ nextjs_space/tests/unit/email-preview.test.ts | 44 ++++++++++++ .../unit/tenant-scope-write-scoping.test.ts | 69 +++++++++++++++++++ 13 files changed, 246 insertions(+), 52 deletions(-) create mode 100644 nextjs_space/tests/unit/tenant-scope-write-scoping.test.ts diff --git a/nextjs_space/app/api/super-admin/email-templates/preview/route.ts b/nextjs_space/app/api/super-admin/email-templates/preview/route.ts index 956105bf..b26b2a3c 100644 --- a/nextjs_space/app/api/super-admin/email-templates/preview/route.ts +++ b/nextjs_space/app/api/super-admin/email-templates/preview/route.ts @@ -67,6 +67,8 @@ export const POST = withSuperAdmin(async (req, { user }) => { category: category !== undefined ? category : owner?.category, tenantId: owner?.tenantId ?? null, businessName: tenant?.businessName, + // Same reason as the tenant route: the srcdoc pane inherits the admin CSP. + baseUrlOverride: req.nextUrl.origin, }); return NextResponse.json({ html }); diff --git a/nextjs_space/app/api/tenant-admin/email-templates/preview/route.ts b/nextjs_space/app/api/tenant-admin/email-templates/preview/route.ts index 495d3ae1..3fe073f3 100644 --- a/nextjs_space/app/api/tenant-admin/email-templates/preview/route.ts +++ b/nextjs_space/app/api/tenant-admin/email-templates/preview/route.ts @@ -74,6 +74,10 @@ export const POST = requirePermission( category: existing ? existing.category : category, tenantId, businessName: tenant?.businessName, + // Assets resolve against the origin the author is on: the pane's + // srcdoc iframe inherits THIS page's CSP, which has no tenant hosts in + // img-src, so tenant-domain URLs would render as broken images. + baseUrlOverride: req.nextUrl.origin, }); return NextResponse.json({ html }); diff --git a/nextjs_space/app/super-admin/emails/[id]/client.tsx b/nextjs_space/app/super-admin/emails/[id]/client.tsx index fc2654ce..06add789 100644 --- a/nextjs_space/app/super-admin/emails/[id]/client.tsx +++ b/nextjs_space/app/super-admin/emails/[id]/client.tsx @@ -65,34 +65,41 @@ export function EditTemplateClient({ } }; + // The same sized-wrapper shape as the tenant-admin email screens: the editor + // is h-full and fills a `flex-1 overflow-hidden` box instead of guessing the + // viewport, so the Save header can never be scrolled out of reach. return ( -
- -