feat: admin frontend - React + Vite, auth pages, user management, roles, permissions, webhooks, devices, logs
This commit is contained in:
201
frontend/admin/src/app/providers/AuthProvider.tsx
Normal file
201
frontend/admin/src/app/providers/AuthProvider.tsx
Normal file
@@ -0,0 +1,201 @@
|
||||
/**
|
||||
* 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>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user