Files
user-system/frontend/admin/src/app/providers/AuthProvider.tsx
long-agent 61c19e54ac fix: P1-02 OAuth context propagation and P1-16 AuthProvider double-check
P1-02: OAuth ExchangeCode and GetUserInfo now accept context parameter
       to properly propagate request context to HTTP calls
P1-16: AuthProvider isAuthenticated now uses single source of truth
       (effectiveUser !== null) instead of double-checking both
       React state and module-level function
2026-04-18 19:40:54 +08:00

202 lines
4.9 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 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<SessionUser | null>(getCurrentUser())
const [roles, setRoles] = useState<Role[]>(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<Role[]> => {
try {
const result = await get<Role[]>(`/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<SessionUser>('/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 (
<AuthContext.Provider value={value}>
{children}
</AuthContext.Provider>
)
}