"use client"; import { createContext, useCallback, useContext, useEffect, useMemo, useState, type ReactNode, } from "react"; const API_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; export interface AuthUser { id: string; username: string; display_name: string; is_admin: boolean; } interface AuthContextValue { user: AuthUser | null; token: string | null; isAuthenticated: boolean; isLoading: boolean; login: (username: string, password: string) => Promise; logout: () => void; } const AuthContext = createContext(null); export function useAuth(): AuthContextValue { const ctx = useContext(AuthContext); if (!ctx) throw new Error("useAuth must be used within AuthProvider"); return ctx; } const TOKEN_KEY = "epeekit-auth-token"; const REFRESH_KEY = "epeekit-refresh-token"; const USER_KEY = "epeekit-auth-user"; export function getStoredToken(): string | null { if (typeof window === "undefined") return null; return localStorage.getItem(TOKEN_KEY); } export function AuthProvider({ children }: { children: ReactNode }) { const [user, setUser] = useState(null); const [token, setToken] = useState(null); const [isLoading, setIsLoading] = useState(true); const saveAuth = useCallback((accessToken: string, refreshToken: string, userData: AuthUser) => { localStorage.setItem(TOKEN_KEY, accessToken); localStorage.setItem(REFRESH_KEY, refreshToken); localStorage.setItem(USER_KEY, JSON.stringify(userData)); setToken(accessToken); setUser(userData); }, []); const clearAuth = useCallback(() => { localStorage.removeItem(TOKEN_KEY); localStorage.removeItem(REFRESH_KEY); localStorage.removeItem(USER_KEY); setToken(null); setUser(null); }, []); // 尝试用 refresh token 获取新 access token const tryRefresh = useCallback(async (): Promise => { const refreshToken = localStorage.getItem(REFRESH_KEY); if (!refreshToken) return false; try { const resp = await fetch(`${API_URL}/api/auth/refresh`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ refresh_token: refreshToken }), }); if (!resp.ok) return false; const data = await resp.json(); const savedUser = localStorage.getItem(USER_KEY); if (savedUser && data.access_token) { const userData = JSON.parse(savedUser) as AuthUser; localStorage.setItem(TOKEN_KEY, data.access_token); setToken(data.access_token); setUser(userData); return true; } return false; } catch { return false; } }, []); // 启动时验证 token useEffect(() => { async function init() { const savedToken = localStorage.getItem(TOKEN_KEY); if (!savedToken) { setIsLoading(false); return; } try { const resp = await fetch(`${API_URL}/api/auth/me`, { headers: { Authorization: `Bearer ${savedToken}` }, }); if (resp.ok) { const userData = await resp.json(); setToken(savedToken); setUser(userData); } else if (resp.status === 401) { const refreshed = await tryRefresh(); if (!refreshed) clearAuth(); } else { clearAuth(); } } catch { // 网络错误时保留本地缓存的用户信息,允许离线使用 const savedUser = localStorage.getItem(USER_KEY); if (savedUser) { setToken(savedToken); setUser(JSON.parse(savedUser)); } } setIsLoading(false); } init(); }, [clearAuth, tryRefresh]); const login = useCallback(async (username: string, password: string) => { const resp = await fetch(`${API_URL}/api/auth/login`, { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ username, password }), }); if (!resp.ok) { const err = await resp.json().catch(() => ({ detail: "登录失败" })); throw new Error(err.detail || "登录失败"); } const data = await resp.json(); saveAuth(data.access_token, data.refresh_token, data.user); }, [saveAuth]); const logout = useCallback(() => { clearAuth(); }, [clearAuth]); const value = useMemo( () => ({ user, token, isAuthenticated: !!token && !!user, isLoading, login, logout, }), [user, token, isLoading, login, logout] ); return {children}; }