(main)/layout.tsx 的客户端守卫判定未登录时只跳 /login,未清 cookie; 而 proxy.ts 仅凭 cookie 是否存在放行,二者在 cookie 存在但 localStorage 为空时互相弹跳 → 无限重定向 → 白屏。跳转前调用 auth.clearToken() 同步 清除两处凭证,与退出登录/401 处理的既有约定保持一致。 Co-Authored-By: Claude Opus 4.8 <[email protected]>
87 lines
3.1 KiB
TypeScript
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>
|
|
);
|
|
}
|