import { useQuery, useQueryClient } from "@tanstack/react-query"; import { createContext, useContext, useEffect, useMemo, useState, type ReactNode } from "react"; import { Navigate, useLocation } from "react-router-dom"; import { api, getToken, LOGOUT_EVENT, setToken } from "../api/client"; import type { Me } from "../api/types"; interface AuthState { user: Me | null; isAdmin: boolean; ready: boolean; login: (u: string, p: string) => Promise; logout: () => void; } const Ctx = createContext(null); export function AuthProvider({ children }: { children: ReactNode }) { const qc = useQueryClient(); const [token, setTok] = useState(() => getToken()); // 401 到期登出与显式登出同样清理:跨用户数据不得残留在 query 缓存 / SW CacheStorage const clearSession = () => { qc.clear(); if (typeof caches !== "undefined") { void caches.delete("booklib-api"); void caches.delete("booklib-immutable"); } }; useEffect(() => { const off = () => { setTok(null); // client 的 401 拦截在这里回收 React 状态(token 已由 client.ts 清除) clearSession(); }; window.addEventListener(LOGOUT_EVENT, off); return () => window.removeEventListener(LOGOUT_EVENT, off); }, []); const meQ = useQuery({ queryKey: ["me"], queryFn: api.me, enabled: !!token, retry: 0 }); useEffect(() => { if (token && meQ.isError) { setToken(null); // 非 401 失败也要清持久化 token,避免与 React 状态分叉导致 RequireAuth 永久加载 // eslint-disable-next-line react-hooks/set-state-in-effect -- 对「me 查询失败」异步结果的清理反应,与持久化 token 需同帧回收;改渲染期重置/事件驱动会改变鉴权失败时序,保守保留 setTok(null); } }, [token, meQ.isError]); const value = useMemo( () => ({ user: meQ.data ?? null, isAdmin: meQ.data?.role === "admin", ready: !token || !meQ.isPending, login: async (u, p) => { const r = await api.login(u, p); setToken(r.token); setTok(r.token); await qc.invalidateQueries({ queryKey: ["me"] }); }, logout: () => { setToken(null); setTok(null); clearSession(); }, }), [meQ.data, meQ.isPending, meQ.isError, token, qc], ); return {children}; } export function useAuth(): AuthState { const v = useContext(Ctx); if (!v) throw new Error("useAuth 必须在 AuthProvider 内使用"); return v; } export function RequireAuth({ children }: { children: ReactNode }) { const { user, ready } = useAuth(); const loc = useLocation(); if (!getToken()) return ; if (!ready || !user) return
加载中…
; return <>{children}; } export function RequireAdmin({ children }: { children: ReactNode }) { const { isAdmin } = useAuth(); return isAdmin ? <>{children} : ; }