fix(auth): handle zero-permission users gracefully and prevent redirect loops
This commit is contained in:
@ -1,5 +1,6 @@
|
||||
import { redirect } from '@tanstack/react-router'
|
||||
import { can, useAuthStore } from '@/auth/store'
|
||||
import { getFirstAccessibleRoute } from '@/components/layout/nav'
|
||||
|
||||
/**
|
||||
* Admin permission keys — mirrors the backend catalog
|
||||
@ -50,6 +51,7 @@ export type Permission = (typeof PERM)[keyof typeof PERM]
|
||||
export function requirePermission(permission: Permission): void {
|
||||
const { user } = useAuthStore.getState()
|
||||
if (user && !can(permission)) {
|
||||
throw redirect({ to: '/' })
|
||||
const target = getFirstAccessibleRoute()
|
||||
throw redirect({ to: target && target !== '/' ? target : '/' })
|
||||
}
|
||||
}
|
||||
|
||||
@ -51,5 +51,8 @@ export function useCan(permission: string): boolean {
|
||||
}
|
||||
|
||||
export function isAuthenticated(): boolean {
|
||||
return Boolean(useAuthStore.getState().accessToken)
|
||||
const { accessToken, user, roles, permissions } = useAuthStore.getState()
|
||||
if (!accessToken) return false
|
||||
if (user && roles.length === 0 && permissions.length === 0) return false
|
||||
return true
|
||||
}
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import type { LucideIcon } from 'lucide-react'
|
||||
import { can } from '@/auth/store'
|
||||
import {
|
||||
BookOpenText,
|
||||
Bot,
|
||||
@ -100,3 +101,15 @@ export const NAV_GROUPS: NavGroup[] = [
|
||||
],
|
||||
},
|
||||
]
|
||||
|
||||
export function getFirstAccessibleRoute(): string | null {
|
||||
for (const group of NAV_GROUPS) {
|
||||
for (const item of group.items) {
|
||||
if (!item.permission || can(item.permission)) {
|
||||
return item.to
|
||||
}
|
||||
}
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@ -2,6 +2,7 @@ import { Link, Outlet, createFileRoute, redirect, useNavigate } from '@tanstack/
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { useEffect } from 'react'
|
||||
import { LogOut, Moon, Sun } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
import { fetchMe } from '@/api/modules/auth'
|
||||
import { can, isAuthenticated, useAuthStore } from '@/auth/store'
|
||||
import { NAV_GROUPS } from '@/components/layout/nav'
|
||||
@ -41,11 +42,19 @@ function AuthedLayout() {
|
||||
|
||||
useEffect(() => {
|
||||
if (me.data) {
|
||||
const roles = me.data.meta?.roles ?? []
|
||||
const permissions = me.data.meta?.permissions ?? []
|
||||
if (roles.length === 0 && permissions.length === 0) {
|
||||
useAuthStore.getState().clear()
|
||||
toast.error('账号未分配管理后台权限,已退出登录')
|
||||
void navigate({ to: '/login' })
|
||||
return
|
||||
}
|
||||
useAuthStore
|
||||
.getState()
|
||||
.setSession(me.data.data, me.data.meta?.roles ?? [], me.data.meta?.permissions ?? [])
|
||||
.setSession(me.data.data, roles, permissions)
|
||||
}
|
||||
}, [me.data])
|
||||
}, [me.data, navigate])
|
||||
|
||||
useEffect(() => {
|
||||
if (me.isError && !isAuthenticated()) {
|
||||
|
||||
@ -1,7 +1,51 @@
|
||||
import { createFileRoute, redirect } from '@tanstack/react-router'
|
||||
import { createFileRoute, redirect, useNavigate } from '@tanstack/react-router'
|
||||
import { ShieldAlert, LogOut } from 'lucide-react'
|
||||
import { getFirstAccessibleRoute } from '@/components/layout/nav'
|
||||
import { useAuthStore } from '@/auth/store'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
|
||||
|
||||
export const Route = createFileRoute('/_authed/')({
|
||||
beforeLoad: () => {
|
||||
throw redirect({ to: '/categories' })
|
||||
const target = getFirstAccessibleRoute()
|
||||
if (target && target !== '/') {
|
||||
throw redirect({ to: target })
|
||||
}
|
||||
},
|
||||
component: DashboardIndexPage,
|
||||
})
|
||||
|
||||
function DashboardIndexPage() {
|
||||
const navigate = useNavigate()
|
||||
const user = useAuthStore((s) => s.user)
|
||||
|
||||
const logout = () => {
|
||||
useAuthStore.getState().clear()
|
||||
void navigate({ to: '/login' })
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="flex min-h-[60vh] items-center justify-center p-4">
|
||||
<Card className="w-full max-w-md text-center shadow-xs">
|
||||
<CardHeader>
|
||||
<div className="mx-auto mb-2 flex size-12 items-center justify-center rounded-full bg-amber-500/10 text-amber-600 dark:bg-amber-500/20 dark:text-amber-400">
|
||||
<ShieldAlert className="size-6" />
|
||||
</div>
|
||||
<CardTitle className="text-xl">账号未分配权限</CardTitle>
|
||||
<CardDescription>
|
||||
当前账号【{user?.display_name ?? user?.username ?? user?.uid}】尚未分配任何管理后台权限
|
||||
</CardDescription>
|
||||
</CardHeader>
|
||||
<CardContent className="space-y-4">
|
||||
<p className="text-xs text-muted-foreground">
|
||||
如果您需要访问管理后台,请联系超级管理员在「用户角色」管理中为您分配相应的角色。
|
||||
</p>
|
||||
<Button variant="outline" onClick={logout} className="gap-2">
|
||||
<LogOut className="size-4" />
|
||||
退出登录
|
||||
</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@ -45,7 +45,13 @@ function LoginPage() {
|
||||
|
||||
const me = await fetchMe()
|
||||
const roles = me.meta?.roles ?? []
|
||||
useAuthStore.getState().setSession(me.data, roles, me.meta?.permissions ?? [])
|
||||
const permissions = me.meta?.permissions ?? []
|
||||
if (roles.length === 0 && permissions.length === 0) {
|
||||
useAuthStore.getState().clear()
|
||||
toast.error('该账号未分配管理后台权限,无法登录')
|
||||
return
|
||||
}
|
||||
useAuthStore.getState().setSession(me.data, roles, permissions)
|
||||
|
||||
await navigate({ to: '/' })
|
||||
} catch (error) {
|
||||
|
||||
Reference in New Issue
Block a user