Files
ARTEX/web/src/app/(main)/layout.tsx
T
Autumn-27andClaude Opus 4.8 3c8a9f74f8 fix(web): 守卫跳转登录页前清除 cookie,修复 cookie 与 localStorage 不一致导致的无限重定向白屏
(main)/layout.tsx 的客户端守卫判定未登录时只跳 /login,未清 cookie;
而 proxy.ts 仅凭 cookie 是否存在放行,二者在 cookie 存在但 localStorage
为空时互相弹跳 → 无限重定向 → 白屏。跳转前调用 auth.clearToken() 同步
清除两处凭证,与退出登录/401 处理的既有约定保持一致。

Co-Authored-By: Claude Opus 4.8 <[email protected]>
2026-08-07 09:43:30 -04:00

87 lines
3.1 KiB
TypeScript

"use client";
import type { ReactNode } from "react";
import * as React from "react";
import { AppSidebar } from "@/app/(main)/_components/sidebar/app-sidebar";
import { SidebarInset, SidebarProvider } from "@/components/ui/sidebar";
import { auth } from "@/lib/auth";
import { getClientCookie } from "@/lib/cookie.client";
import {
SIDEBAR_COLLAPSIBLE_VALUES,
SIDEBAR_VARIANT_VALUES,
type SidebarCollapsible,
type SidebarVariant,
} from "@/lib/preferences/layout";
import { PREFERENCE_DEFAULTS } from "@/lib/preferences/preferences-config";
import { cn } from "@/lib/utils";
import { MainContent } from "./_components/main-content";
// Reads a layout-critical preference from the browser cookie (falls back to the
// default during static-export prerender where document is unavailable). Kept
// fully client-side so the app can be statically exported — no Server Actions /
// next/headers.
function readPref<T extends string>(key: string, allowed: readonly T[], fallback: T): T {
if (typeof document === "undefined") return fallback;
const value = getClientCookie(key);
return value && (allowed as readonly string[]).includes(value) ? (value as T) : fallback;
}
export default function Layout({ children }: Readonly<{ children: ReactNode }>) {
// Client-side auth gate — replaces the Next proxy/middleware that static export
// disables. No token → bounce to /login; render nothing until confirmed so no
// protected UI (or its API calls) flashes for a logged-out visitor.
const [authed, setAuthed] = React.useState(false);
React.useEffect(() => {
if (auth.getToken()) {
setAuthed(true);
} else {
// 客户端守卫认为未登录时,必须同时清掉 cookie:否则 proxy.ts 仅凭
// “cookie 存在”就把我们从 /login 又重定向回主界面,与本守卫来回弹跳
// 形成无限重定向 → 白屏(cookie 与 localStorage 不一致时触发)。
auth.clearToken();
window.location.href = "/login";
}
}, []);
const defaultOpen = typeof document === "undefined" ? true : getClientCookie("sidebar_state") !== "false";
const variant = readPref<SidebarVariant>(
"sidebar_variant",
SIDEBAR_VARIANT_VALUES,
PREFERENCE_DEFAULTS.sidebar_variant,
);
const collapsible = readPref<SidebarCollapsible>(
"sidebar_collapsible",
SIDEBAR_COLLAPSIBLE_VALUES,
PREFERENCE_DEFAULTS.sidebar_collapsible,
);
if (!authed) return null;
return (
<SidebarProvider
defaultOpen={defaultOpen}
style={
{
"--sidebar-width": "calc(var(--spacing) * 68)",
} as React.CSSProperties
}
>
<AppSidebar variant={variant} collapsible={collapsible} />
<SidebarInset
className={cn(
"[html[data-content-layout=centered]_&>*]:mx-auto",
"[html[data-content-layout=centered]_&>*]:w-full",
"[html[data-content-layout=centered]_&>*]:max-w-screen-2xl",
"peer-data-[variant=inset]:border",
"[--dashboard-header-height:--spacing(12)]",
"min-w-0 overflow-x-hidden",
)}
>
<MainContent>{children}</MainContent>
</SidebarInset>
</SidebarProvider>
);
}