202 lines
5.0 KiB
TypeScript
202 lines
5.0 KiB
TypeScript
|
|
/**
|
|||
|
|
* 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 && isAuthenticated(),
|
|||
|
|
isLoading,
|
|||
|
|
onLoginSuccess,
|
|||
|
|
logout,
|
|||
|
|
refreshUser,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
return (
|
|||
|
|
<AuthContext.Provider value={value}>
|
|||
|
|
{children}
|
|||
|
|
</AuthContext.Provider>
|
|||
|
|
)
|
|||
|
|
}
|