// ============================================================ // Auth Context — 认证状态管理 // 职责:登录/注册/登出/token 刷新,为子组件提供 auth 状态 // ============================================================ import { createContext, useCallback, useContext, useEffect, useMemo, useRef, useState, type ReactNode, } from "react"; import * as api from "./api"; import type { AuthUser } from "./api"; import { clearAuth, loadAccessToken, loadRefreshToken, loadUser, saveAccessToken, saveRefreshToken, saveUser, } from "./storage"; interface AuthState { user: AuthUser | null; isAuthenticated: boolean; isLoading: boolean; } interface AuthContextValue extends AuthState { login: (username: string, password: string) => Promise<{ error?: string }>; register: (username: string, password: string) => Promise<{ error?: string }>; logout: () => Promise; accessToken: string | null; } const AuthContext = createContext(null); /** Access token 提前刷新的缓冲时间(秒) */ const REFRESH_BUFFER_SEC = 60; /** 解析 JWT payload(不做签名验证) */ function parseJwtPayload(token: string): { exp?: number } | null { try { const base64 = token.split(".")[1]; const json = atob(base64.replace(/-/g, "+").replace(/_/g, "/")); return JSON.parse(json); } catch { return null; } } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(loadUser); const [accessToken, setAccessToken] = useState(loadAccessToken); const [isLoading, setIsLoading] = useState(true); const refreshTimerRef = useRef | null>(null); // 清除定时器 const clearRefreshTimer = useCallback(() => { if (refreshTimerRef.current) { clearTimeout(refreshTimerRef.current); refreshTimerRef.current = null; } }, []); // 持久化 token + user const persistAuth = useCallback( (authUser: AuthUser, access: string, refresh: string) => { setUser(authUser); setAccessToken(access); saveAccessToken(access); saveRefreshToken(refresh); saveUser(authUser); }, [] ); // 安排自动刷新 const scheduleRefresh = useCallback( (access: string) => { clearRefreshTimer(); const payload = parseJwtPayload(access); if (!payload?.exp) return; const nowSec = Math.floor(Date.now() / 1000); const delayMs = Math.max((payload.exp - nowSec - REFRESH_BUFFER_SEC) * 1000, 5000); refreshTimerRef.current = setTimeout(async () => { const rt = loadRefreshToken(); if (!rt) return; const res = await api.refreshToken(rt); if (res.data) { persistAuth(res.data.user, res.data.access_token, res.data.refresh_token); scheduleRefresh(res.data.access_token); } else { // 刷新失败,清除 auth clearAuth(); setUser(null); setAccessToken(null); } }, delayMs); }, [clearRefreshTimer, persistAuth] ); // 初始化:检查已有 token 并尝试刷新 useEffect(() => { const init = async () => { const storedAccess = loadAccessToken(); const storedRefresh = loadRefreshToken(); const storedUser = loadUser(); if (!storedAccess || !storedRefresh || !storedUser) { setIsLoading(false); return; } // 检查 access token 是否过期 const payload = parseJwtPayload(storedAccess); const nowSec = Math.floor(Date.now() / 1000); if (payload?.exp && payload.exp > nowSec) { // access token 仍然有效 setUser(storedUser); setAccessToken(storedAccess); scheduleRefresh(storedAccess); } else { // access token 过期,尝试 refresh const res = await api.refreshToken(storedRefresh); if (res.data) { persistAuth(res.data.user, res.data.access_token, res.data.refresh_token); scheduleRefresh(res.data.access_token); } else { clearAuth(); } } setIsLoading(false); }; init(); return () => clearRefreshTimer(); }, []); // eslint-disable-line react-hooks/exhaustive-deps const login = useCallback( async (username: string, password: string): Promise<{ error?: string }> => { const res = await api.login(username, password); if (res.data) { persistAuth(res.data.user, res.data.access_token, res.data.refresh_token); scheduleRefresh(res.data.access_token); return {}; } return { error: res.error?.message || "登录失败" }; }, [persistAuth, scheduleRefresh] ); const register = useCallback( async (username: string, password: string): Promise<{ error?: string }> => { const res = await api.register(username, password); if (res.data) { persistAuth(res.data.user, res.data.access_token, res.data.refresh_token); scheduleRefresh(res.data.access_token); return {}; } return { error: res.error?.message || "注册失败" }; }, [persistAuth, scheduleRefresh] ); const logout = useCallback(async () => { const at = accessToken; const rt = loadRefreshToken(); if (at && rt) { await api.logout(at, rt); } clearRefreshTimer(); clearAuth(); setUser(null); setAccessToken(null); }, [accessToken, clearRefreshTimer]); const value = useMemo( () => ({ user, isAuthenticated: !!user && !!accessToken, isLoading, login, register, logout, accessToken, }), [user, accessToken, isLoading, login, register, logout] ); return {children}; } export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) { throw new Error("useAuth must be used within an AuthProvider"); } return ctx; }