// The editor shell — and the AUTH GATE. This layout wraps every page in the // silent `(app)` group (URLs `/`), 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. import type { ReactNode } from 'react' import { RedirectError, useLoaderData, type LoaderFn } from '@voltro/web' import { T, useLocale, useT } from '@voltro/i18n' import { LocaleSwitcher } from '@voltro/ui-shadcn' import type { Identity } from '../../lib/api' const LOCALES = [ { code: 'en', label: 'English' }, { code: 'de', label: 'Deutsch' }, ] interface AppData { 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 — and bounce // an anonymous caller to /login. if (query) { const me = await query('session.me', {}) if (me.type !== 'user') { throw new RedirectError('/login?from=/') } return { tenantId: me.tenantId } } // Client navigation: the entry load already gated; every rpc is independently // session-authed on the server (the HttpOnly cookie isn't readable in JS). return { tenantId: null } } export default function AppLayout({ children }: { readonly children: ReactNode }): ReactNode { useLoaderData() const locale = useLocale() const langLabel = useT('lang.label') const signOut = (): void => { void fetch('/auth/csrf') .then((r) => r.json() as Promise<{ csrfToken: string }>) .then((c) => fetch('/auth/sign-out', { method: 'POST', headers: { 'x-csrf-token': c.csrfToken } }), ) .finally(() => window.location.assign('/login')) } return (
{children}
) }