- 新增 AdminSessionRecover:后台离线/后端重启时自动重试恢复会话,明确失效才回跳登录 - 认证 cookie 契约调整(SameSite=Lax 与刷新轮转适配),路由与守卫适配 - 访问来源统计查询优化与测试、管理端 analytics sources 页面适配 - 移动端布局修正:覆盖 body min-h-screen 避免内容区高度异常
100 lines
3.8 KiB
TypeScript
100 lines
3.8 KiB
TypeScript
import type { Metadata } from "next";
|
||
import Link from "next/link";
|
||
import { cookies, headers } from "next/headers";
|
||
import { redirect } from "next/navigation";
|
||
import { ShieldAlert } from "lucide-react";
|
||
import { authCookieHeader, REFRESH_COOKIE, CSRF_COOKIE, TOKEN_COOKIE } from "@/lib/cookies";
|
||
import { getMeCached, getPublicSettingsCached } from "@/lib/serverData";
|
||
import { canAccessAdminMessages, isStaff } from "@/lib/roles";
|
||
import AdminShell from "@/components/admin/AdminShell";
|
||
import AdminSessionRecover from "./AdminSessionRecover";
|
||
|
||
// 整个 /admin 树不进入索引(子页面无需重复声明)
|
||
export const metadata: Metadata = {
|
||
robots: { index: false, follow: false },
|
||
};
|
||
|
||
// 权限以 Go 端为唯一判定;此处仅决定管理界面是否直出。
|
||
// 允许:管理角色,或仅有消息管理/群管权限的用户(仅消息菜单可见)。
|
||
export default async function AdminLayout({ children }: { children: React.ReactNode }) {
|
||
const cookieStore = await cookies();
|
||
const cookie = authCookieHeader(cookieStore);
|
||
const me = await getMeCached(cookie || undefined);
|
||
const settings = await getPublicSettingsCached();
|
||
const theme = cookieStore.get("j13-theme")?.value === "dark" ? "dark" : "light";
|
||
|
||
const allowed =
|
||
!!me.user && (isStaff(me.user.role) || canAccessAdminMessages(me.user));
|
||
|
||
if (!allowed || !me.user) {
|
||
// 已登录但无后台权限:保留提示卡片
|
||
if (me.user) {
|
||
return (
|
||
<div
|
||
data-admin-viewport
|
||
id="main"
|
||
tabIndex={-1}
|
||
className="min-h-screen flex items-center justify-center px-4 outline-none"
|
||
>
|
||
<div className="max-w-md w-full panel p-8 text-center">
|
||
<span
|
||
className="inline-flex w-14 h-14 rounded-full items-center justify-center mb-4"
|
||
style={{ background: "var(--gold-soft)", color: "var(--gold)" }}
|
||
>
|
||
<ShieldAlert size={26} />
|
||
</span>
|
||
<h1 className="text-lg font-extrabold" style={{ color: "var(--ink)" }}>
|
||
需要管理员权限
|
||
</h1>
|
||
<p className="meta mt-2 text-[13px]">此区域仅供站点管理员访问,如有疑问请联系站长。</p>
|
||
<Link href="/" className="btn btn-primary mt-6">
|
||
返回首页
|
||
</Link>
|
||
</div>
|
||
</div>
|
||
);
|
||
}
|
||
|
||
// SSR 侧未识别出用户(/api/me 瞬断、access 过期轮转竞态等)时,原实现直接
|
||
// 落到无壳的权限卡片——手机端表现为「首次进入后台整块导航 header 消失」,
|
||
// 无法切换管理板块,再进一次才恢复。这里按凭据状态分流入修复:
|
||
// ① 完全无凭据:直达登录页并回跳目标地址,不再给无导航的死胡同卡片;
|
||
// ② 仍带凭据:客户端经 fetchWithRefresh 自动续期后 router.refresh 自愈。
|
||
const hasAuthHint = !!(
|
||
cookieStore.get(TOKEN_COOKIE)?.value ||
|
||
cookieStore.get(REFRESH_COOKIE)?.value ||
|
||
cookieStore.get(CSRF_COOKIE)?.value
|
||
);
|
||
if (!hasAuthHint) {
|
||
const pathname = (await headers()).get("x-pathname") || "/admin";
|
||
redirect(`/login?redirect=${encodeURIComponent(pathname)}`);
|
||
}
|
||
return (
|
||
<div
|
||
data-admin-viewport
|
||
id="main"
|
||
tabIndex={-1}
|
||
className="min-h-screen flex items-center justify-center px-4 outline-none"
|
||
>
|
||
<AdminSessionRecover />
|
||
</div>
|
||
);
|
||
}
|
||
|
||
const user = me.user;
|
||
|
||
// 未读仅在有 access cookie 时才可信;壳层铃铛用 SSR 直出避免挂载后才出红点
|
||
const unread = cookieStore.get(TOKEN_COOKIE)?.value ? me.unread_count : 0;
|
||
|
||
return (
|
||
<AdminShell
|
||
user={user}
|
||
siteName={settings.site_name || "姜十三论坛"}
|
||
initialTheme={theme}
|
||
initialUnread={unread}
|
||
>
|
||
{children}
|
||
</AdminShell>
|
||
);
|
||
}
|