feat(branding): multi-tenant brand naming + per-port email shell + auth UI continuity
Removes the last hardcoded "Port Nimara" references so a tenant cloning
the deploy with a fresh slug sees their own brand throughout.
Browser + native chrome:
- `generateMetadata` reads `branding_app_name` from the first port row
so the browser tab title, apple-web-app title, and template literal
reflect the tenant (fallback "CRM" until DB is seeded).
- Mobile topbar derives the brand-mark initials from the port slug
("port-nimara" → "PN", "marina-alpha" → "MA") — no code edit on clone.
- `documenso-payload` default redirect URL is `""` so Documenso falls
back to its own post-sign page instead of routing every tenant's
signers to portnimara.com; per-port `redirectUrl` setting still wins.
- Server-startup log uses generic "CRM server listening".
Email + auth shell:
- New `auth-shell-branding.ts` resolves logo / background / appName once
per request from `system_settings`; used by both the email shell and
the auth-pages SSR layout.
- `auth-branding-provider` wraps `/login`, `/reset-password`, `/set-password`,
portal `/portal/*` so the branded shell hydrates with the same assets
the inbox sees.
- `me/email` change email uses the branded shell instead of inline HTML
with "Port Nimara CRM" baked into copy.
- Admin branding page adds an email-preview card (POSTs to
`/api/v1/admin/branding/email-preview`) so an admin can spot-check
their templates before going live.
- `/api/public/files/[id]` exposes branding-category files anonymously
so inbox images (no session cookie) can render; any other category
still flows through authenticated `/api/v1/files/[id]/preview`.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,9 @@ import { getPortalDashboard } from '@/lib/services/portal.service';
|
||||
import { isPortalDisabledGlobally } from '@/lib/services/portal-auth.service';
|
||||
import { PortalHeader } from '@/components/portal/portal-header';
|
||||
import { PortalNav } from '@/components/portal/portal-nav';
|
||||
import { AuthBrandingProvider } from '@/components/shared/auth-branding-provider';
|
||||
import { resolveAuthShellBranding } from '@/lib/email/auth-shell-branding';
|
||||
import { getPortBrandingConfig } from '@/lib/services/port-config';
|
||||
|
||||
export const metadata: Metadata = {
|
||||
title: {
|
||||
@@ -54,15 +57,31 @@ export default async function PortalLayout({ children }: { children: React.React
|
||||
}
|
||||
}
|
||||
|
||||
// Branding for the auth-shell pages (login, forgot-password, reset).
|
||||
// When the visitor has a session, use that port's branding so they
|
||||
// stay inside one tenant's look. Otherwise pick up the first-port
|
||||
// default — the same path the CRM auth pages take.
|
||||
const branding = session
|
||||
? await getPortBrandingConfig(session.portId)
|
||||
.then((cfg) => ({
|
||||
logoUrl: cfg.logoUrl,
|
||||
backgroundUrl: cfg.emailBackgroundUrl,
|
||||
appName: cfg.appName,
|
||||
}))
|
||||
.catch(() => null)
|
||||
: await resolveAuthShellBranding();
|
||||
|
||||
return (
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{session && (
|
||||
<>
|
||||
<PortalHeader portName={portName} portLogoUrl={portLogoUrl} clientName={clientName} />
|
||||
<PortalNav />
|
||||
</>
|
||||
)}
|
||||
<main className={session ? 'max-w-5xl mx-auto px-4 sm:px-6 py-8' : ''}>{children}</main>
|
||||
</div>
|
||||
<AuthBrandingProvider branding={branding}>
|
||||
<div className="min-h-screen bg-gray-50">
|
||||
{session && (
|
||||
<>
|
||||
<PortalHeader portName={portName} portLogoUrl={portLogoUrl} clientName={clientName} />
|
||||
<PortalNav />
|
||||
</>
|
||||
)}
|
||||
<main className={session ? 'max-w-5xl mx-auto px-4 sm:px-6 py-8' : ''}>{children}</main>
|
||||
</div>
|
||||
</AuthBrandingProvider>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user