import type { Metadata } from 'next'; import { getPortalSession } from '@/lib/portal/auth'; 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: { default: 'Client Portal', template: '%s | Client Portal', }, }; export default async function PortalLayout({ children }: { children: React.ReactNode }) { // Route-level kill switch. When every port has client_portal_enabled=false, // surface a clean "Portal not available" notice instead of letting the // login form render (it would just reject every submit with a confusing // ConflictError). Single-port deployments effectively get a global toggle // out of the admin System Settings UI. if (await isPortalDisabledGlobally()) { return (

Client portal unavailable

The client portal isn't currently enabled for this site. If you were expecting to sign in here, please contact your account manager.

); } // This layout wraps all portal routes including login/verify // We can't easily check pathname in a server layout, so we attempt // to get the session and pass it down - login/verify pages handle their own // redirect logic independently. const session = await getPortalSession().catch(() => null); // For authenticated routes we need client info for the header. // If session is absent, children (login/verify pages) handle their own redirect. let clientName = ''; let portName = 'Client Portal'; let portLogoUrl: string | null = null; if (session) { const dashboard = await getPortalDashboard(session.clientId, session.portId).catch(() => null); if (dashboard) { clientName = dashboard.client.fullName; portName = dashboard.port.name; portLogoUrl = dashboard.port.logoUrl; } } // 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 (
{session && ( <> )}
{children}
); }