refactor(web): 优化认证布局和权限检查
- auth layout 添加智能重定向(检查权限后重定向到可访问页面) - dashboard layout 优化权限加载和过期检查逻辑 - 支持权限缓存过期后自动刷新 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -1,37 +1,111 @@
|
||||
'use client';
|
||||
|
||||
import type { MenuTreeNode } from '@seclusion/shared';
|
||||
import Link from 'next/link';
|
||||
import { useRouter, useSearchParams } from 'next/navigation';
|
||||
import { Suspense, useEffect, useState } from 'react';
|
||||
|
||||
import { ThemeToggle } from '@/components/layout/ThemeToggle';
|
||||
import { siteConfig } from '@/config/site';
|
||||
import { getUserMenusAndPermissions } from '@/services/permission.service';
|
||||
import { useAuthStore, usePermissionStore } from '@/stores';
|
||||
|
||||
export default function AuthLayout({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
function AuthRedirect() {
|
||||
const router = useRouter();
|
||||
const searchParams = useSearchParams();
|
||||
const { token, isHydrated: authHydrated } = useAuthStore();
|
||||
const { isLoaded, isExpired, setPermissionData, canAccessRoute, menus } = usePermissionStore();
|
||||
const [hasRedirected, setHasRedirected] = useState(false);
|
||||
|
||||
// 加载权限数据
|
||||
useEffect(() => {
|
||||
if (!authHydrated || !token) return;
|
||||
|
||||
if (!isLoaded || isExpired()) {
|
||||
getUserMenusAndPermissions()
|
||||
.then((data) => {
|
||||
setPermissionData(data);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('加载权限数据失败:', error);
|
||||
setPermissionData({ menus: [], permissions: [], isSuperAdmin: false });
|
||||
});
|
||||
}
|
||||
}, [authHydrated, token, isLoaded, isExpired, setPermissionData]);
|
||||
|
||||
// 执行智能重定向(只执行一次)
|
||||
useEffect(() => {
|
||||
// 未登录、权限未加载或已经执行过重定向,则不处理
|
||||
if (!authHydrated || !token || !isLoaded || hasRedirected) return;
|
||||
|
||||
const targetRedirect = searchParams.get('redirect') || '/dashboard';
|
||||
|
||||
// 检查目标路由是否可访问
|
||||
if (canAccessRoute(targetRedirect)) {
|
||||
setHasRedirected(true);
|
||||
router.replace(targetRedirect);
|
||||
return;
|
||||
}
|
||||
|
||||
// 目标路由不可访问,找到第一个可访问的菜单路径
|
||||
const findFirstAccessiblePath = (menuList: MenuTreeNode[]): string | null => {
|
||||
for (const menu of menuList) {
|
||||
// 跳过隐藏菜单、外部链接和没有路径的菜单
|
||||
if (!menu.isHidden && menu.path && !menu.isExternal) {
|
||||
return menu.path;
|
||||
}
|
||||
// 递归查找子菜单
|
||||
if (menu.children && menu.children.length > 0) {
|
||||
const childPath = findFirstAccessiblePath(menu.children);
|
||||
if (childPath) return childPath;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const firstPath = findFirstAccessiblePath(menus);
|
||||
|
||||
setHasRedirected(true);
|
||||
if (firstPath) {
|
||||
router.replace(firstPath);
|
||||
} else {
|
||||
// 没有任何可访问的路由,跳转到无权限页面
|
||||
router.replace('/403');
|
||||
}
|
||||
}, [authHydrated, token, isLoaded, hasRedirected, searchParams, canAccessRoute, menus, router]);
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
export default function AuthLayout({ children }: { children: React.ReactNode }) {
|
||||
return (
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* 顶部导航 */}
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-14 items-center justify-between">
|
||||
<Link href="/" className="font-bold text-xl">
|
||||
{siteConfig.name}
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
<>
|
||||
<Suspense fallback={null}>
|
||||
<AuthRedirect />
|
||||
</Suspense>
|
||||
<div className="min-h-screen flex flex-col">
|
||||
{/* 顶部导航 */}
|
||||
<header className="sticky top-0 z-50 w-full border-b bg-background/95 backdrop-blur supports-[backdrop-filter]:bg-background/60">
|
||||
<div className="container flex h-14 items-center justify-between">
|
||||
<Link href="/" className="font-bold text-xl">
|
||||
{siteConfig.name}
|
||||
</Link>
|
||||
<ThemeToggle />
|
||||
</div>
|
||||
</header>
|
||||
|
||||
{/* 主内容区 */}
|
||||
<main className="flex-1 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">{children}</div>
|
||||
</main>
|
||||
{/* 主内容区 */}
|
||||
<main className="flex-1 flex items-center justify-center p-4">
|
||||
<div className="w-full max-w-md">{children}</div>
|
||||
</main>
|
||||
|
||||
{/* 页脚 */}
|
||||
<footer className="border-t py-4">
|
||||
<div className="container text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} {siteConfig.name}. All rights
|
||||
reserved.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
{/* 页脚 */}
|
||||
<footer className="border-t py-4">
|
||||
<div className="container text-center text-sm text-muted-foreground">
|
||||
© {new Date().getFullYear()} {siteConfig.name}. All rights reserved.
|
||||
</div>
|
||||
</footer>
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useEffect } from 'react';
|
||||
|
||||
import { Header } from '@/components/layout/Header';
|
||||
import { Sidebar, MobileSidebar } from '@/components/layout/Sidebar';
|
||||
import { getRoutePermission, isPublicRoute } from '@/config/route-permissions';
|
||||
import { getLoginUrl } from '@/lib/auth';
|
||||
import { getUserMenusAndPermissions } from '@/services/permission.service';
|
||||
import { useAuthStore, usePermissionStore } from '@/stores';
|
||||
@@ -14,7 +13,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
const pathname = usePathname();
|
||||
const router = useRouter();
|
||||
const { token, isHydrated: authHydrated } = useAuthStore();
|
||||
const { isLoaded, setPermissionData, hasPermission, clearPermissionData } = usePermissionStore();
|
||||
const { isLoaded, isExpired, setPermissionData, canAccessRoute, clearPermissionData } =
|
||||
usePermissionStore();
|
||||
|
||||
// 登录状态检查:未登录时重定向到登录页
|
||||
useEffect(() => {
|
||||
@@ -26,8 +26,8 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
return;
|
||||
}
|
||||
|
||||
// 已登录且权限未加载时,加载权限数据
|
||||
if (!isLoaded) {
|
||||
// 已登录且权限未加载或已过期时,加载/刷新权限数据
|
||||
if (!isLoaded || isExpired()) {
|
||||
getUserMenusAndPermissions()
|
||||
.then((data) => {
|
||||
setPermissionData(data);
|
||||
@@ -38,24 +38,17 @@ export default function DashboardLayout({ children }: { children: React.ReactNod
|
||||
setPermissionData({ menus: [], permissions: [], isSuperAdmin: false });
|
||||
});
|
||||
}
|
||||
}, [authHydrated, token, isLoaded, pathname, router, setPermissionData, clearPermissionData]);
|
||||
}, [authHydrated, token, isLoaded, isExpired, pathname, router, setPermissionData, clearPermissionData]);
|
||||
|
||||
// 路由权限检查
|
||||
useEffect(() => {
|
||||
if (!authHydrated || !isLoaded) return;
|
||||
|
||||
// 公开路由不检查权限
|
||||
if (isPublicRoute(pathname)) return;
|
||||
|
||||
// 获取路由所需权限
|
||||
const requiredPermission = getRoutePermission(pathname);
|
||||
if (!requiredPermission) return;
|
||||
|
||||
// 检查权限
|
||||
if (!hasPermission(requiredPermission)) {
|
||||
// 检查是否可以访问当前路由
|
||||
if (!canAccessRoute(pathname)) {
|
||||
router.replace('/403');
|
||||
}
|
||||
}, [authHydrated, isLoaded, pathname, hasPermission, router]);
|
||||
}, [authHydrated, isLoaded, pathname, canAccessRoute, router]);
|
||||
|
||||
return (
|
||||
<div className="min-h-screen flex">
|
||||
|
||||
Reference in New Issue
Block a user