/** * AuthProvider - 全局会话上下文 * * 提供: * - 会话状态(user, roles, isAdmin) * - 登录/登出方法 * - 会话恢复(启动时自动刷新) */ import { useEffect, useState, useCallback, type ReactNode, } from 'react' import { useNavigate } from 'react-router-dom' import type { SessionUser, Role, TokenBundle } from '@/types' import { get } from '@/lib/http' import { setRefreshToken, clearRefreshToken, hasSessionPresenceCookie, } from '@/lib/storage' import { setAccessToken, getCurrentUser, setCurrentUser, getCurrentRoles, setCurrentRoles, clearSession, isAuthenticated, isAccessTokenExpired, } from '@/lib/http/auth-session' import { initCSRFToken, clearCSRFToken } from '@/lib/http/csrf' import { logout as logoutRequest, refreshSession } from '@/services/auth' import { AuthContext, type AuthContextValue } from './auth-context' // ==================== Provider ==================== interface AuthProviderProps { children: ReactNode } export function AuthProvider({ children }: AuthProviderProps) { const [user, setUser] = useState(getCurrentUser()) const [roles, setRoles] = useState(getCurrentRoles()) const [isLoading, setIsLoading] = useState(true) const navigate = useNavigate() const effectiveUser = user ?? getCurrentUser() const effectiveRoles = roles.length > 0 ? roles : getCurrentRoles() // 判断是否为管理员 const isAdmin = effectiveRoles.some((role) => role.code === 'admin') /** * 获取用户角色 */ const fetchUserRoles = useCallback(async (userId: number): Promise => { try { const result = await get(`/users/${userId}/roles`) return result } catch { return [] } }, []) /** * 登录成功回调 */ const onLoginSuccess = useCallback(async (tokenBundle: TokenBundle) => { // 保存 tokens setAccessToken(tokenBundle.access_token, tokenBundle.expires_in) setRefreshToken(tokenBundle.refresh_token) // 保存用户信息 setCurrentUser(tokenBundle.user) setUser(tokenBundle.user) // 获取角色 const userRoles = await fetchUserRoles(tokenBundle.user.id) setCurrentRoles(userRoles) setRoles(userRoles) // 初始化 CSRF Token await initCSRFToken() }, [fetchUserRoles]) /** * 刷新用户信息 */ const refreshUser = useCallback(async () => { try { const userInfo = await get('/auth/userinfo') setCurrentUser(userInfo) setUser(userInfo) const userRoles = await fetchUserRoles(userInfo.id) setCurrentRoles(userRoles) setRoles(userRoles) } catch { // 刷新失败,清除会话 setUser(null) setRoles([]) } }, [fetchUserRoles]) /** * 登出 */ const logout = useCallback(async () => { try { await logoutRequest() } catch { // 忽略登出请求错误 } finally { // 无论请求成功与否,都清除本地会话和 CSRF Token clearRefreshToken() clearSession() clearCSRFToken() setUser(null) setRoles([]) navigate('/login') } }, [navigate]) /** * 会话恢复(应用启动时,只运行一次) */ useEffect(() => { const restoreSession = async () => { // 如果已有 access_token 且未过期,直接使用 if (isAuthenticated() && !isAccessTokenExpired()) { const currentUser = getCurrentUser() const currentRoles = getCurrentRoles() if (currentUser) { setUser(currentUser) setRoles(currentRoles) await initCSRFToken() setIsLoading(false) return } } if (!hasSessionPresenceCookie()) { clearRefreshToken() clearSession() setUser(null) setRoles([]) setIsLoading(false) return } try { const result = await refreshSession() // 保存 tokens setAccessToken(result.access_token, result.expires_in) setRefreshToken(result.refresh_token) // 保存用户信息 setCurrentUser(result.user) setUser(result.user) // 获取角色 const userRoles = await fetchUserRoles(result.user.id) setCurrentRoles(userRoles) setRoles(userRoles) await initCSRFToken() } catch { // 刷新失败,清除会话 clearRefreshToken() clearSession() setUser(null) setRoles([]) } setIsLoading(false) } restoreSession() // eslint-disable-next-line react-hooks/exhaustive-deps }, []) // 只在挂载时运行一次,不依赖 location.pathname const value: AuthContextValue = { user: effectiveUser, roles: effectiveRoles, isAdmin, isAuthenticated: effectiveUser !== null, isLoading, onLoginSuccess, logout, refreshUser, } return ( {children} ) }