diff --git a/frontend/admin/src/app/providers/AuthProvider.test.tsx b/frontend/admin/src/app/providers/AuthProvider.test.tsx
index 2b2ed53..8158ea3 100644
--- a/frontend/admin/src/app/providers/AuthProvider.test.tsx
+++ b/frontend/admin/src/app/providers/AuthProvider.test.tsx
@@ -239,6 +239,26 @@ describe('AuthProvider', () => {
expect(screen.getByTestId('roles')).toHaveTextContent('admin')
})
+ it('keeps provider roles stable when the module session store changes after mount', async () => {
+ storedAccessToken = 'cached-access-token'
+ storedUser = operatorUser
+ storedRoles = []
+ isAccessTokenExpiredMock.mockReturnValue(false)
+
+ const view = renderAuthProvider()
+ await waitForProviderIdle()
+ expect(screen.getByTestId('roles').textContent).toBe('')
+
+ storedRoles = adminRoles
+ view.rerender(
+
+
+ ,
+ )
+
+ expect(screen.getByTestId('roles').textContent).toBe('')
+ })
+
it('clears the local session when auth state has no current user and no backend session cookie exists', async () => {
storedAccessToken = 'dangling-access-token'
isAuthenticatedMock.mockReturnValue(true)
diff --git a/frontend/admin/src/app/providers/AuthProvider.tsx b/frontend/admin/src/app/providers/AuthProvider.tsx
index 072aa06..5be8db3 100644
--- a/frontend/admin/src/app/providers/AuthProvider.tsx
+++ b/frontend/admin/src/app/providers/AuthProvider.tsx
@@ -46,11 +46,9 @@ export function AuthProvider({ children }: AuthProviderProps) {
const [roles, setRoles] = useState(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 isAdmin = roles.some((role) => role.code === 'admin')
/**
* 获取用户角色
@@ -64,6 +62,31 @@ export function AuthProvider({ children }: AuthProviderProps) {
}
}, [])
+ const applyAuthState = useCallback((nextUser: SessionUser | null, nextRoles: Role[]) => {
+ setUser(nextUser)
+ setRoles(nextRoles)
+ }, [])
+
+ const clearLocalAuthState = useCallback(() => {
+ applyAuthState(null, [])
+ }, [applyAuthState])
+
+ const persistSessionUser = useCallback((nextUser: SessionUser) => {
+ setCurrentUser(nextUser)
+ setUser(nextUser)
+ }, [])
+
+ const persistSessionRoles = useCallback((nextRoles: Role[]) => {
+ setCurrentRoles(nextRoles)
+ setRoles(nextRoles)
+ }, [])
+
+ const loadRolesForUser = useCallback(async (userId: number): Promise => {
+ const userRoles = await fetchUserRoles(userId)
+ persistSessionRoles(userRoles)
+ return userRoles
+ }, [fetchUserRoles, persistSessionRoles])
+
/**
* 登录成功回调
*/
@@ -71,19 +94,14 @@ export function AuthProvider({ children }: AuthProviderProps) {
// 保存 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)
-
+
+ // 保存用户信息与角色
+ persistSessionUser(tokenBundle.user)
+ await loadRolesForUser(tokenBundle.user.id)
+
// 初始化 CSRF Token
await initCSRFToken()
- }, [fetchUserRoles])
+ }, [loadRolesForUser, persistSessionUser])
/**
* 刷新用户信息
@@ -91,18 +109,12 @@ export function AuthProvider({ children }: AuthProviderProps) {
const refreshUser = useCallback(async () => {
try {
const userInfo = await get('/auth/userinfo')
- setCurrentUser(userInfo)
- setUser(userInfo)
-
- const userRoles = await fetchUserRoles(userInfo.id)
- setCurrentRoles(userRoles)
- setRoles(userRoles)
+ persistSessionUser(userInfo)
+ await loadRolesForUser(userInfo.id)
} catch {
- // 刷新失败,清除会话
- setUser(null)
- setRoles([])
+ // 保留当前 provider 状态,避免短暂的 userinfo 抖动清空已登录会话
}
- }, [fetchUserRoles])
+ }, [loadRolesForUser, persistSessionUser])
/**
* 登出
@@ -117,11 +129,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
clearRefreshToken()
clearSession()
clearCSRFToken()
- setUser(null)
- setRoles([])
+ clearLocalAuthState()
navigate('/login')
}
- }, [navigate])
+ }, [clearLocalAuthState, navigate])
/**
* 会话恢复(应用启动时,只运行一次)
@@ -132,10 +143,9 @@ export function AuthProvider({ children }: AuthProviderProps) {
if (isAuthenticated() && !isAccessTokenExpired()) {
const currentUser = getCurrentUser()
const currentRoles = getCurrentRoles()
-
+
if (currentUser) {
- setUser(currentUser)
- setRoles(currentRoles)
+ applyAuthState(currentUser, currentRoles)
await initCSRFToken()
setIsLoading(false)
return
@@ -145,8 +155,7 @@ export function AuthProvider({ children }: AuthProviderProps) {
if (!hasSessionPresenceCookie()) {
clearRefreshToken()
clearSession()
- setUser(null)
- setRoles([])
+ clearLocalAuthState()
setIsLoading(false)
return
}
@@ -158,21 +167,15 @@ export function AuthProvider({ children }: AuthProviderProps) {
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)
+ // 保存用户信息与角色
+ persistSessionUser(result.user)
+ await loadRolesForUser(result.user.id)
await initCSRFToken()
} catch {
// 刷新失败,清除会话
clearRefreshToken()
clearSession()
- setUser(null)
- setRoles([])
+ clearLocalAuthState()
}
setIsLoading(false)
@@ -183,10 +186,10 @@ export function AuthProvider({ children }: AuthProviderProps) {
}, []) // 只在挂载时运行一次,不依赖 location.pathname
const value: AuthContextValue = {
- user: effectiveUser,
- roles: effectiveRoles,
+ user,
+ roles,
isAdmin,
- isAuthenticated: effectiveUser !== null,
+ isAuthenticated: user !== null,
isLoading,
onLoginSuccess,
logout,