feat: 登录设备管理与编辑器图片上传
- 新增登录设备列表与会话下线(ListLoginDevices/RevokeLoginDevice),按设备指纹识别当前会话 - Markdown 编辑器新增图片插入弹窗与图片上传(ImageInsertModal) - 实时总线、账号守卫与用户事件适配
This commit is contained in:
@@ -2,77 +2,160 @@
|
||||
|
||||
import { useEffect, useState } from "react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Ban } from "lucide-react";
|
||||
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)" }}
|
||||
<>
|
||||
<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
|
||||
>
|
||||
<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"
|
||||
我知道了
|
||||
</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>
|
||||
</Modal>
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user