Files
user-system/frontend/admin/src/app/providers/AuthProvider.tsx

202 lines
4.9 KiB
TypeScript
Raw Normal View History

/**
* 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>
)
}