Files
jiang13-bbs/frontend/components/AccountGuard.tsx
freefire 1d862a0dd3 feat: 站点媒体库与移动端底部导航,服务测试补强
- 新增媒体库:media_library/media_thumbs 服务(WebP 缩略图)、管理端 media 页面
- 移动端底部导航 MobileTabBar 替换 MobilePostBar
- 旧数据导入增强与测试、路由与登录会话/板块侧边栏测试补强
- site-doc 组件精简(移除 Breadcrumb),文档新增迁移公告说明
2026-09-27 02:36:11 +08:00

162 lines
5.3 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"use client";
import { useEffect, useState } from "react";
import { useRouter } from "next/navigation";
import { Ban, LogOut } from "lucide-react";
import { onForceLogout } from "@/lib/userEvents";
import { onDocumentVisible } from "@/lib/visibilityReconcile";
import { hasAuthCookieHint } from "@/lib/cookies";
import { apiMe } from "@/lib/api";
import { realtime, RT_SESSION_REPLACED } from "@/lib/realtime";
import Modal from "@/components/Modal";
/**
* 账号安全守卫:监听全局强制下线事件,弹出不可误关的告知模态。
*
* 两类告知:
* - 账号被封禁(reason=banned):管理员封禁后由 HTTP 拦截链路派发;
* - 登录已失效(reason=session_replaced):安全设置剔除本设备、
* 会话数超上限被淘汰等场景,旧浏览器当场弹窗提示,而非等硬刷新才发现。
*
* 登录态被顶的感知入口(探测统一收敛在 lib/api 的 apiMe 判定):
* 1) 后端 WS 即时事件 session:replaced(旧设备 WS 在线时秒级感知);
* 2) WS 心跳复查失败 4401(realtime 内部转一次 /api/me 探测);
* 3) 回前台对账:后台标签页错过 WS 事件,切回时经 /api/me 校正。
*
* 登录态清理(cookie/Header 游客化/router.refresh)由 Header 复用统一
* 登出链路完成;本组件只负责"告知",故不处理任何凭据逻辑。
* 挂载于 root layout,覆盖任意页面(含后台)。
*/
export default function AccountGuard() {
const router = useRouter();
const [banned, setBanned] = useState(false);
const [sessionEnded, setSessionEnded] = useState(false);
useEffect(
() =>
onForceLogout((reason) => {
if (reason === "banned") setBanned(true);
else if (reason === "session_replaced") setSessionEnded(true);
}),
[]
);
// 登录态被顶:后端 WS 即时事件 → 探测确认 → 派发 session_replaced
useEffect(
() => realtime.on(RT_SESSION_REPLACED, () => void apiMe().catch(() => {})),
[]
);
// 回前台对账(10s 限频):覆盖后台标签页错过 WS 事件的场景。
// 仅当存在登录 cookie 痕迹时探测,游客不白发请求
useEffect(
() =>
onDocumentVisible(() => {
if (hasAuthCookieHint()) void apiMe().catch(() => {});
}),
[]
);
return (
<>
<Modal
open={banned}
title="账号已被封禁"
onClose={() => {}}
hideClose
closeOnEscape={false}
role="alertdialog"
ariaLabel="账号已被封禁"
maxWidthClass="max-w-[400px]"
footer={
<button
type="button"
onClick={() => {
setBanned(false);
router.push("/");
}}
className="btn btn-primary w-full"
autoFocus
>
我知道了
</button>
}
header={
<div className="w-full text-center">
<span
className="mx-auto w-14 h-14 rounded-full flex items-center justify-center"
style={{ background: "var(--danger-soft)", color: "var(--danger)" }}
>
<Ban size={26} />
</span>
<h2
id="banned-dialog-title"
className="mt-4 text-[18px] font-extrabold tracking-tight"
style={{ color: "var(--ink)" }}
>
账号已被封禁
</h2>
</div>
}
>
<p
id="banned-dialog-desc"
className="meta text-[13px] leading-relaxed text-center"
>
你的账号因违反社区规则已被管理员封禁,当前登录状态已失效,无法继续发布内容与互动。
如对处理结果有异议,请联系管理员核实。
</p>
</Modal>
<Modal
open={sessionEnded}
title="登录已失效"
onClose={() => {}}
hideClose
closeOnEscape={false}
role="alertdialog"
ariaLabel="登录已失效"
maxWidthClass="max-w-[400px]"
footer={
<button
type="button"
onClick={() => {
setSessionEnded(false);
router.push("/login");
}}
className="btn btn-primary w-full"
autoFocus
>
重新登录
</button>
}
header={
<div className="w-full text-center">
<span
className="mx-auto w-14 h-14 rounded-full flex items-center justify-center"
style={{ background: "var(--accent-soft)", color: "var(--accent)" }}
>
<LogOut size={26} />
</span>
<h2
id="session-ended-dialog-title"
className="mt-4 text-[18px] font-extrabold tracking-tight"
style={{ color: "var(--ink)" }}
>
登录已失效
</h2>
</div>
}
>
<p
id="session-ended-dialog-desc"
className="meta text-[13px] leading-relaxed text-center"
>
你的登录会话已被移除(在安全设置中剔除,或会话数超出上限被淘汰),当前设备的登录状态已失效。
请重新登录后继续使用。
</p>
</Modal>
</>
);
}