feat: 登录设备管理与编辑器图片上传
- 新增登录设备列表与会话下线(ListLoginDevices/RevokeLoginDevice),按设备指纹识别当前会话 - Markdown 编辑器新增图片插入弹窗与图片上传(ImageInsertModal) - 实时总线、账号守卫与用户事件适配
This commit is contained in:
@@ -660,6 +660,19 @@ let refreshPromise: Promise<{ ok: boolean; banned: boolean }> | null = null;
|
||||
// 重新登录成功(apiLogin)后复位,使"解封后再次被封"仍能再次提示
|
||||
let bannedNotified = false;
|
||||
|
||||
// 登录态被顶/被剔除通知去重(其他浏览器登录、安全设置剔除设备等)。
|
||||
// sessionSeen 标记本标签页曾处于登录态:游客触发 401 不应弹"登录已失效"
|
||||
let sessionEndedNotified = false;
|
||||
let sessionSeen = false;
|
||||
|
||||
// 曾登录、经探测确认已游客化 → 派发 session_replaced 强制下线事件
|
||||
function notifySessionEnded(): void {
|
||||
if (sessionEndedNotified || !sessionSeen) return;
|
||||
sessionEndedNotified = true;
|
||||
sessionSeen = false;
|
||||
emitForceLogout("session_replaced");
|
||||
}
|
||||
|
||||
// 识别响应体中的封禁 code;body 只能读一次,故 clone 探测,原响应照常交给调用方
|
||||
function detectBannedBody(res: Response): void {
|
||||
if (res.status !== 403 || bannedNotified) return;
|
||||
@@ -717,6 +730,10 @@ async function fetchWithRefresh(url: string, init: RequestInit): Promise<Respons
|
||||
} else if (result.banned && !bannedNotified) {
|
||||
bannedNotified = true;
|
||||
emitForceLogout("banned");
|
||||
} else if (!result.banned) {
|
||||
// refresh 失败且非封禁:登录态已失效(被顶/被剔除/过期),
|
||||
// 曾登录的标签页统一告知,避免界面仍显示已登录
|
||||
notifySessionEnded();
|
||||
}
|
||||
}
|
||||
return res;
|
||||
@@ -747,6 +764,22 @@ export async function fetchPosts(
|
||||
return res.json();
|
||||
}
|
||||
|
||||
// 编辑器站内链接:按关键词搜帖子(公开接口;keyword 留空返回最近发布)
|
||||
export async function apiSearchPosts(
|
||||
keyword = "",
|
||||
size = 8
|
||||
): Promise<{ posts?: PostListItem[]; error?: string }> {
|
||||
const params = new URLSearchParams({ page: "1", size: String(size), sort: "latest" });
|
||||
if (keyword) params.set("keyword", keyword);
|
||||
const res = await fetchWithRefresh(`/api/posts?${params}`, {
|
||||
method: "GET",
|
||||
headers: clientHeaders(),
|
||||
});
|
||||
if (!res.ok) return { error: "搜索帖子失败" };
|
||||
const data = (await res.json().catch(() => ({}))) as Partial<PostsResponse>;
|
||||
return { posts: data.posts || [] };
|
||||
}
|
||||
|
||||
// SSR 首页聚合数据;失败时抛错,由首页调用方 try/catch 降级为无侧栏模块
|
||||
export async function fetchOverview(cookieHeader?: string): Promise<OverviewResponse> {
|
||||
const res = await fetch(`${API_BASE}/api/overview`, ssrInit(cookieHeaders(cookieHeader)));
|
||||
@@ -1302,10 +1335,15 @@ export async function apiMe(): Promise<MeResponse> {
|
||||
cache: "no-store",
|
||||
});
|
||||
const data: MeResponse = await res.json();
|
||||
if (data.user) sessionSeen = true;
|
||||
// /me 以 200 携带封禁标记(Header 挂载静默校正/F5 后的主识别路径)
|
||||
if (data.banned === true && !bannedNotified) {
|
||||
bannedNotified = true;
|
||||
emitForceLogout("banned");
|
||||
} else if (!data.user && res.ok) {
|
||||
// 曾登录、现在明确是游客:登录态在别处被顶/被剔除(其他浏览器登录、
|
||||
// 设备剔除等)。挂载校正/回前台对账/WS 4401 探测都汇聚到这条判定
|
||||
notifySessionEnded();
|
||||
}
|
||||
return data;
|
||||
}
|
||||
@@ -1318,8 +1356,12 @@ export async function apiLogin(username: string, password: string) {
|
||||
body: JSON.stringify({ username, password }),
|
||||
});
|
||||
const data = await res.json();
|
||||
// 新登录态开始:复位封禁通知去重(解封后再次被封仍可提示)
|
||||
if (res.ok && data?.user) bannedNotified = false;
|
||||
// 新登录态开始:复位封禁/被顶通知去重(解封后再次被封、再次被顶仍可提示)
|
||||
if (res.ok && data?.user) {
|
||||
bannedNotified = false;
|
||||
sessionEndedNotified = false;
|
||||
sessionSeen = true;
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user