feat: admin frontend - React + Vite, auth pages, user management, roles, permissions, webhooks, devices, logs

This commit is contained in:
2026-04-02 11:20:20 +08:00
parent dcc1f186f8
commit 4718980ab5
235 changed files with 35682 additions and 0 deletions

View 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>
)
}