= ({ children }) => {
- const { isAuthenticated } = useAppSelector((state) => state.auth);
+ const dispatch = useAppDispatch();
+ const { isAuthenticated, token, initialized, loading, permissions } = useAppSelector(
+ (state) => state.auth
+ );
const location = useLocation();
+ const requiredPermission = resolveRequiredPermissionByPath(location.pathname);
+
+ React.useEffect(() => {
+ if (initialized || loading) {
+ return;
+ }
+ if (!token) {
+ dispatch(markInitialized());
+ return;
+ }
+ void dispatch(fetchCurrentUser());
+ }, [dispatch, initialized, loading, token]);
+
+ if (!initialized || loading) {
+ return null;
+ }
if (!isAuthenticated) {
// Redirect to the login page, but save the current location they were trying to go to
return ;
}
+ if (!hasPermission(permissions, requiredPermission)) {
+ const fallbackPath = resolveDefaultAuthorizedPath(permissions);
+ if (location.pathname === fallbackPath) {
+ return ;
+ }
+ return ;
+ }
+
return children ? <>{children}> : ;
};
diff --git a/frontend/src/pages/Forbidden/ForbiddenPage.tsx b/frontend/src/pages/Forbidden/ForbiddenPage.tsx
new file mode 100644
index 0000000..671d66e
--- /dev/null
+++ b/frontend/src/pages/Forbidden/ForbiddenPage.tsx
@@ -0,0 +1,24 @@
+import React from "react";
+import { Button, Result } from "antd";
+import { useNavigate } from "react-router";
+
+const ForbiddenPage: React.FC = () => {
+ const navigate = useNavigate();
+ return (
+
+ navigate("/data/management")}>
+ 返回首页
+
+ }
+ />
+
+ );
+};
+
+export default ForbiddenPage;
+
diff --git a/frontend/src/pages/Layout/Sidebar.tsx b/frontend/src/pages/Layout/Sidebar.tsx
index f93ceee..57d135c 100644
--- a/frontend/src/pages/Layout/Sidebar.tsx
+++ b/frontend/src/pages/Layout/Sidebar.tsx
@@ -1,4 +1,4 @@
-import { memo, useCallback, useEffect, useState } from "react";
+import { memo, useCallback, useEffect, useMemo, useState } from "react";
import { Button, Drawer, Menu, Popover } from "antd";
import {
CloseOutlined,
@@ -14,6 +14,7 @@ import SettingsPage from "../SettingsPage/SettingsPage";
import { useAppSelector, useAppDispatch } from "@/store/hooks";
import { showSettings, hideSettings } from "@/store/slices/settingsSlice";
import { logout } from "@/store/slices/authSlice";
+import { hasPermission } from "@/auth/permissions";
const isPathMatch = (currentPath: string, targetPath: string) =>
currentPath === targetPath || currentPath.startsWith(`${targetPath}/`);
@@ -25,13 +26,36 @@ const AsiderAndHeaderLayout = () => {
const [sidebarOpen, setSidebarOpen] = useState(true);
const [taskCenterVisible, setTaskCenterVisible] = useState(false);
const settingVisible = useAppSelector((state) => state.settings.visible);
+ const permissions = useAppSelector((state) => state.auth.permissions);
const dispatch = useAppDispatch();
+ const visibleMenuItems = useMemo(
+ () =>
+ menuItems
+ .map((item) => ({
+ ...item,
+ children: item.children?.filter((subItem) =>
+ hasPermission(permissions, (subItem as { permissionCode?: string }).permissionCode)
+ ),
+ }))
+ .filter((item) => {
+ const selfVisible = hasPermission(
+ permissions,
+ (item as { permissionCode?: string }).permissionCode
+ );
+ if (item.children && item.children.length > 0) {
+ return selfVisible;
+ }
+ return selfVisible;
+ }),
+ [permissions]
+ );
+
// Initialize active item based on current pathname
const initActiveItem = useCallback(() => {
const dataPath = pathname.startsWith("/data/") ? pathname.slice(6) : pathname;
- for (let index = 0; index < menuItems.length; index++) {
- const element = menuItems[index];
+ for (let index = 0; index < visibleMenuItems.length; index++) {
+ const element = visibleMenuItems[index];
if (element.children) {
for (const subItem of element.children) {
if (isPathMatch(dataPath, subItem.id)) {
@@ -44,7 +68,8 @@ const AsiderAndHeaderLayout = () => {
return;
}
}
- }, [pathname]);
+ setActiveItem(visibleMenuItems[0]?.id ?? "");
+ }, [pathname, visibleMenuItems]);
useEffect(() => {
initActiveItem();
@@ -100,7 +125,7 @@ const AsiderAndHeaderLayout = () => {