import { createContext, useContext, useState, useCallback, useMemo, useEffect, type ReactNode, } from "react"; import { login as loginApi, getMe, logout as logoutApi } from "../services/authApi"; // ============================================================ // 类型 // ============================================================ export interface UserInfo { id: string; username: string; display_name: string; role: string; } interface AuthState { user: UserInfo | null; token: string | null; loading: boolean; } interface AuthContextValue extends AuthState { login: (username: string, password: string) => Promise; logout: () => void; isAuthenticated: boolean; } // ============================================================ // Token 存储 key // ============================================================ const ACCESS_TOKEN_KEY = "track_admin_token"; const REFRESH_TOKEN_KEY = "track_admin_refresh_token"; const USER_KEY = "track_admin_user"; // ============================================================ // Context // ============================================================ const AuthContext = createContext(null); export function useAuth() { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth 必须在 AuthProvider 内使用"); return ctx; } // ============================================================ // Provider // ============================================================ export function AuthProvider({ children }: { children: ReactNode }) { const [state, setState] = useState({ user: null, token: null, loading: true, }); // 初始化:从 localStorage 恢复双 Token useEffect(() => { const savedToken = localStorage.getItem(ACCESS_TOKEN_KEY); const savedUser = localStorage.getItem(USER_KEY); if (savedToken && savedUser) { try { const user = JSON.parse(savedUser) as UserInfo; setState({ user, token: savedToken, loading: false }); // 后台静默验证 token 是否仍有效 getMe(savedToken) .then((fresh) => { setState((prev) => ({ ...prev, user: fresh })); localStorage.setItem(USER_KEY, JSON.stringify(fresh)); }) .catch((err) => { // 验证失败不踢出用户 — 真正的过期由业务 API 401 拦截器 // 通过 Refresh Token 无感刷新,彻底失败才跳转登录 console.error( "[Auth] Token 后台验证失败(保留本地登录态,依赖拦截器刷新):", err?.message ?? err, ); }); } catch { logoutInternal(); } } else { setState((prev) => ({ ...prev, loading: false })); } }, []); function logoutInternal() { localStorage.removeItem(ACCESS_TOKEN_KEY); localStorage.removeItem(REFRESH_TOKEN_KEY); localStorage.removeItem(USER_KEY); setState({ user: null, token: null, loading: false }); } const login = useCallback(async (username: string, password: string) => { const result = await loginApi(username, password); const accessToken = result.access_token; const refreshToken = result.refresh_token; const user: UserInfo = result.user; // 存储双 Token localStorage.setItem(ACCESS_TOKEN_KEY, accessToken); localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken); localStorage.setItem(USER_KEY, JSON.stringify(user)); setState({ user, token: accessToken, loading: false }); }, []); const logout = useCallback(() => { // 先上报登出(审计留痕),再做本地清理 —— 顺序不能反: // api 的请求拦截器是从 localStorage 取 token 的,清掉之后就发不出这个请求了。 // 刻意不 await、失败也不阻断:JWT 无状态,服务端本就不需要它成功, // 而用户点了「退出」必须退得掉,不能因为一次上报失败就卡住。 logoutApi().catch(() => {}); logoutInternal(); }, []); const ctxValue = useMemo( () => ({ ...state, login, logout, isAuthenticated: !!state.token && !!state.user, }), [state, login, logout], ); return ( {children} ); }