readFromStorage validated token shape but never checked exp, so an expired token mounted the full authed shell and every API call 401d silently. Decode the JWT and treat an expired token as no session. Pattern backported from skyai-finance. Frontend audit 2026-06-20, rank 1. Also clears the localStorage Session in onUnauthorized (root.tsx) so a 401 fully logs out instead of leaving a dead session behind getToken. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import {
|
|
Links,
|
|
Meta,
|
|
Outlet,
|
|
Scripts,
|
|
ScrollRestoration,
|
|
isRouteErrorResponse,
|
|
} from "react-router"
|
|
|
|
import type { Route } from "./+types/root"
|
|
import "./app.css"
|
|
|
|
import { ToastProvider, Toaster } from "@crema/notification-ui"
|
|
import { CommandBusProvider } from "@crema/action-bus"
|
|
import { ArcadiaProvider } from "@crema/arcadia-client"
|
|
import { LlmConfigBootstrap } from "~/lib/llm-config-bootstrap"
|
|
import { ProfileBootstrap } from "~/lib/profile-bootstrap"
|
|
import { signOut } from "~/lib/session"
|
|
// CREMA:PROVIDERS-IMPORTS
|
|
|
|
const ARCADIA_URL = import.meta.env.VITE_ARCADIA_URL ?? "http://localhost:4000"
|
|
const ARCADIA_TENANT = import.meta.env.VITE_ARCADIA_TENANT ?? "default"
|
|
|
|
export function Layout({ children }: { children: React.ReactNode }) {
|
|
return (
|
|
<html lang="en" suppressHydrationWarning>
|
|
<head>
|
|
<meta charSet="utf-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
|
<Meta />
|
|
<Links />
|
|
<script
|
|
dangerouslySetInnerHTML={{
|
|
__html: `(function(){try{var t=localStorage.getItem('crema-theme');if(!t)t='dark';if(t==='dark')document.documentElement.classList.add('dark');var f=localStorage.getItem('crema-font-scale');if(!f||!/^(sm|md|lg|xl)$/.test(f))f='sm';document.documentElement.dataset.fontScale=f;var b=localStorage.getItem('crema-bg');if(b&&/^(drift|static)$/.test(b)){document.addEventListener('DOMContentLoaded',function(){document.body.dataset.bg=b;});}var s=localStorage.getItem('crema-surface');if(s&&/^(snow|stone|sage|slate)$/.test(s)){document.addEventListener('DOMContentLoaded',function(){document.body.dataset.surface=s;});}}catch(e){}})();`,
|
|
}}
|
|
/>
|
|
</head>
|
|
<body suppressHydrationWarning>
|
|
<div data-slot="aurora-field" aria-hidden="true">
|
|
<div className="aurora-blob aurora-blob-1" />
|
|
<div className="aurora-blob aurora-blob-2" />
|
|
</div>
|
|
{children}
|
|
<ScrollRestoration />
|
|
<Scripts />
|
|
</body>
|
|
</html>
|
|
)
|
|
}
|
|
|
|
export default function App() {
|
|
return (
|
|
/* CREMA:PROVIDERS-WRAP-OPEN */
|
|
<ToastProvider>
|
|
<ArcadiaProvider
|
|
baseUrl={ARCADIA_URL}
|
|
initialTenantId={ARCADIA_TENANT}
|
|
getToken={() => (typeof window === "undefined" ? null : sessionStorage.getItem("arcadia_access_token"))}
|
|
onUnauthorized={() => {
|
|
if (typeof window !== "undefined") {
|
|
sessionStorage.removeItem("arcadia_access_token")
|
|
sessionStorage.removeItem("arcadia_refresh_token")
|
|
// Also clear the localStorage Session (crema.session); otherwise
|
|
// useSession() still reports "logged in" after a 401 and the shell
|
|
// keeps mounting with a dead token. (Frontend audit 2026-06-20.)
|
|
signOut()
|
|
}
|
|
}}
|
|
>
|
|
<CommandBusProvider>
|
|
<LlmConfigBootstrap />
|
|
<ProfileBootstrap />
|
|
<Outlet />
|
|
<Toaster />
|
|
</CommandBusProvider>
|
|
</ArcadiaProvider>
|
|
</ToastProvider>
|
|
/* CREMA:PROVIDERS-WRAP-CLOSE */
|
|
)
|
|
}
|
|
|
|
export function ErrorBoundary({ error }: Route.ErrorBoundaryProps) {
|
|
let message = "Oops!"
|
|
let details = "An unexpected error occurred."
|
|
let stack: string | undefined
|
|
|
|
if (isRouteErrorResponse(error)) {
|
|
message = error.status === 404 ? "404" : "Error"
|
|
details =
|
|
error.status === 404
|
|
? "The requested page could not be found."
|
|
: error.statusText || details
|
|
} else if (import.meta.env.DEV && error && error instanceof Error) {
|
|
details = error.message
|
|
stack = error.stack
|
|
}
|
|
|
|
return (
|
|
<main className="container mx-auto p-4 pt-16">
|
|
<h1>{message}</h1>
|
|
<p>{details}</p>
|
|
{stack && (
|
|
<pre className="w-full overflow-x-auto p-4">
|
|
<code>{stack}</code>
|
|
</pre>
|
|
)}
|
|
</main>
|
|
)
|
|
}
|