Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions docs/specs/SPEC-006-studio-runtime-and-ui.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,13 @@ and `renderMdxPreview(...)`. The backend bootstrap/runtime publication model
stays unchanged; it still serves the signed runtime bundle and does not publish
the component catalog.

Cookie-authenticated Studio routes must redirect to `/admin/login` when the
session bootstrap request cannot verify the active browser session, including
`401` responses and network-level failures. The redirect includes `returnTo`
for the current Studio route. Token-authenticated embeds must not redirect to
the login route; token authentication failures remain inline operator-facing
states because the host application, not the Studio login form, owns the token.

MDCMS built-in MDX components defined by `SPEC-007` are injected by the Studio
shell/runtime without host registration. Studio resolves their preview
components from MDCMS-owned React code before falling back to host-registered
Expand Down
34 changes: 34 additions & 0 deletions packages/studio/src/lib/runtime-ui/app/admin/layout.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import {
getDefaultAdminSidebarCollapsed,
getAdminSidebarStorageKey,
isDocumentEditorPathname,
resolveAdminLayoutLoginRedirectPath,
} from "./layout.js";

function createContext(): StudioMountContext {
Expand Down Expand Up @@ -171,6 +172,39 @@ test("createAdminLayoutTokenErrorState ignores non-auth status codes", () => {
assert.equal(createAdminLayoutTokenErrorState(null), null);
});

test("resolveAdminLayoutLoginRedirectPath redirects cookie session failures to login", () => {
assert.equal(
resolveAdminLayoutLoginRedirectPath({
isTokenMode: false,
pathname: "/admin/content/page/home",
sessionState: { status: "error", message: "Failed to fetch" },
}),
"/admin/login?returnTo=%2Fadmin%2Fcontent%2Fpage%2Fhome",
);
});

test("resolveAdminLayoutLoginRedirectPath preserves cookie unauthenticated redirects", () => {
assert.equal(
resolveAdminLayoutLoginRedirectPath({
isTokenMode: false,
pathname: "/admin/schema",
sessionState: { status: "unauthenticated" },
}),
"/admin/login?returnTo=%2Fadmin%2Fschema",
);
});

test("resolveAdminLayoutLoginRedirectPath keeps token auth failures inline", () => {
assert.equal(
resolveAdminLayoutLoginRedirectPath({
isTokenMode: true,
pathname: "/admin/content/page/home",
sessionState: { status: "error", message: "Failed to fetch" },
}),
null,
);
});

test("AdminTokenErrorStateView renders retry action and technical details", () => {
const markup = renderToStaticMarkup(
createElement(AdminTokenErrorStateView, {
Expand Down
47 changes: 38 additions & 9 deletions packages/studio/src/lib/runtime-ui/app/admin/layout.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -206,6 +206,29 @@ export function createAdminLayoutTokenErrorState(
return null;
}

export function resolveAdminLayoutLoginRedirectPath(input: {
sessionState: StudioSessionState;
isTokenMode: boolean;
pathname: string;
}): string | null {
if (input.isTokenMode) {
return null;
}

if (
input.sessionState.status !== "unauthenticated" &&
input.sessionState.status !== "error"
) {
return null;
}

const returnTo = encodeURIComponent(
input.pathname.includes("/admin") ? input.pathname : "/admin",
);

return `/admin/login?returnTo=${returnTo}`;
}

export function AdminTokenErrorStateView({
state,
context,
Expand Down Expand Up @@ -551,18 +574,20 @@ function useAdminLayoutRoutedElement({

const environments = environmentsQuery.data?.data ?? [];

// Auth gate: redirect only truly unauthenticated cookie-mode users to login.
// Token-mode embeds must never redirect to the login screen — token auth
// failures are shown inline via the "token-error" session state.
const loginRedirectPath = resolveAdminLayoutLoginRedirectPath({
sessionState,
isTokenMode,
pathname,
});

// Token-mode embeds must never redirect to the login screen; token auth
// failures are shown inline because the host app owns the bearer token.
useEffect(() => {
if (sessionState.status === "unauthenticated" && !isTokenMode) {
const returnTo = encodeURIComponent(
pathname.includes("/admin") ? pathname : "/admin",
);
replace(`/admin/login?returnTo=${returnTo}`);
if (loginRedirectPath) {
replace(loginRedirectPath);
}
return () => {};
}, [sessionState.status, pathname, replace, isTokenMode]);
}, [loginRedirectPath, replace]);

const mdxCatalog = useMemo<MdxComponentCatalog>(
() => context.mdx?.catalog ?? { components: [] },
Expand All @@ -588,6 +613,10 @@ function useAdminLayoutRoutedElement({
);
}

if (loginRedirectPath) {
return null;
}

if (sessionState.status === "unauthenticated") {
return null;
}
Expand Down
Loading