// The dashboard shell — and the AUTH GATE. This nested layout wraps every page // under `/dashboard`, and its `loader` runs server-side BEFORE the pages render. // It asks the api who the caller is (`session.me`) and throws RedirectError to // /login for anyone not signed in. The pages under here are `renderMode: 'ssr'` // precisely so this runs per request — a `static` page can't redirect // per-visitor, and the build rejects a RedirectError there. import type { ReactNode } from 'react' import { RedirectError, useLoaderData, useLocation, type LoaderFn } from '@voltro/web' import { T, useLocale, useT } from '@voltro/i18n' import { LocaleSwitcher } from '@voltro/ui-shadcn' import { APP_NAME } from '../../config' import type { Identity } from '../../lib/api' const LOCALES = [ { code: 'en', label: 'English' }, { code: 'de', label: 'Deutsch' }, ] interface DashboardData { readonly tenantId: string | null } export const loader: LoaderFn = async ({ query }) => { // `query` is present ONLY server-side (the SSR full-page load). There, ask the // api's `session.me` — it forwards the request's session cookie, so the SAME // Subject resolves as the WS path — and bounce an anonymous caller to /login. if (query) { const me = await query('session.me', {}) if (me.type !== 'user') { throw new RedirectError('/login?from=/dashboard') } return { tenantId: me.tenantId } } // Client-side navigation within /dashboard: the entry load already gated, and // every rpc/subscription is independently session-authed on the server (the // HttpOnly cookie isn't readable in JS), so we don't re-check here. return { tenantId: null } } export default function DashboardLayout({ children }: { readonly children: ReactNode }): ReactNode { useLoaderData() const pathname = useLocation() const locale = useLocale() const langLabel = useT('lang.label') const navItem = (href: string, label: ReactNode): ReactNode => ( {label} ) return (
{children}
) }