All checks were successful
Build and Deploy / build-and-deploy (push) Successful in 3m54s
NextAuth 用 JWT 策略,cookie 签名不会因 DB user 被删而失效,
导致 dev 清数据后浏览器仍显示"已登录"但拉不到任何数据(假登录)。
useSyncMe 现在识别 /api/me 的 401/NOT_FOUND/UNAUTHORIZED 三种信号,
命中后调用 signOut({ redirect: false }) + reset(),把 UI 切回未登录态。
生产环境不会清 user,主要受益是 dev/staging 重置数据后无需手动清浏览器 cookie。
Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
83 lines
2.8 KiB
TypeScript
83 lines
2.8 KiB
TypeScript
"use client";
|
|
|
|
import { useEffect, useRef } from "react";
|
|
import { useSession, signOut } from "next-auth/react";
|
|
import { useVoteStore } from "@/lib/store";
|
|
|
|
/**
|
|
* 把服务端 /api/me 的真相态同步进本地 vote store。
|
|
*
|
|
* - status === "authenticated" → 拉一次 /api/me,用 votedArtists 覆盖本地
|
|
* - status === "unauthenticated" → 清本地(避免上一个用户的票残留给下一个登录者)
|
|
* - 切换用户(uid 变化) → 重新拉一次
|
|
*
|
|
* 僵尸 session 兜底:NextAuth 用 JWT 策略,cookie 不会因 DB user 被删而失效。
|
|
* 当 /api/me 返回 401(签名失效) 或 NOT_FOUND(DB 里 user 已不存在) 时,
|
|
* 自动 signOut() 清 cookie —— 避免页面"假登录"假象(显示已登录但拉不到数据)。
|
|
*
|
|
* localStorage 仅作为本设备的缓存加速首屏渲染,服务端永远是唯一真相源。
|
|
*/
|
|
export function useSyncMe() {
|
|
const { data, status } = useSession();
|
|
const hydrateFromServer = useVoteStore((s) => s.hydrateFromServer);
|
|
const reset = useVoteStore((s) => s.reset);
|
|
const lastSyncedUidRef = useRef<string | null>(null);
|
|
|
|
const sessionUser = data?.user as { id?: string } | undefined;
|
|
const uid = sessionUser?.id ?? null;
|
|
|
|
useEffect(() => {
|
|
if (status === "loading") return;
|
|
|
|
if (status === "unauthenticated") {
|
|
if (lastSyncedUidRef.current !== null) {
|
|
reset();
|
|
lastSyncedUidRef.current = null;
|
|
}
|
|
return;
|
|
}
|
|
|
|
// authenticated
|
|
if (!uid) return;
|
|
if (lastSyncedUidRef.current === uid) return;
|
|
|
|
let cancelled = false;
|
|
const ctrl = new AbortController();
|
|
const timer = setTimeout(() => ctrl.abort(), 8000);
|
|
|
|
fetch("/api/me", {
|
|
credentials: "include",
|
|
signal: ctrl.signal,
|
|
})
|
|
.then(async (r) => {
|
|
if (cancelled) return;
|
|
const res = await r.json().catch(() => null);
|
|
|
|
if (r.ok && res?.ok && Array.isArray(res.data?.votedArtists)) {
|
|
hydrateFromServer(res.data.votedArtists as string[]);
|
|
lastSyncedUidRef.current = uid;
|
|
return;
|
|
}
|
|
|
|
// 僵尸 session:JWT 还有效,但 DB 里 user 已不存在(或鉴权失效)。
|
|
// 直接登出清 cookie,UI 状态切换为未登录,避免"显示已登录但拉不到数据"。
|
|
const code: string | undefined = res?.error?.code;
|
|
if (r.status === 401 || code === "UNAUTHORIZED" || code === "NOT_FOUND") {
|
|
signOut({ redirect: false });
|
|
reset();
|
|
lastSyncedUidRef.current = null;
|
|
}
|
|
})
|
|
.catch(() => {
|
|
// 网络失败容忍 —— 下次 status 变化或手动刷新会再试
|
|
})
|
|
.finally(() => clearTimeout(timer));
|
|
|
|
return () => {
|
|
cancelled = true;
|
|
ctrl.abort();
|
|
clearTimeout(timer);
|
|
};
|
|
}, [status, uid, hydrateFromServer, reset]);
|
|
}
|