diff --git a/frontend/src/hooks/useAvailableMenuItems.ts b/frontend/src/hooks/useAvailableMenuItems.ts new file mode 100644 index 00000000..f1bfd09d --- /dev/null +++ b/frontend/src/hooks/useAvailableMenuItems.ts @@ -0,0 +1,82 @@ +/* + * Copyright © 2025 Hexastack. All rights reserved. + * + * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: + * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. + * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). + */ + +import { useMemo } from "react"; + +import { MenuItem } from "@/layout/VerticalMenu"; +import { EntityType } from "@/services/types"; +import { PermissionAction } from "@/types/permission.types"; + +import { useHasPermission } from "./useHasPermission"; + +/** + * Helper function to check permissions for a menu item + * @param menuItem - The menu item + * @param hasPermission - Callback function + * @returns True if hasPermission() is true for all required permissions. + */ +const isMenuItemAllowed = ( + menuItem: MenuItem, + hasPermission: (entityType: EntityType, action: PermissionAction) => boolean, +): boolean => { + const requiredPermissions = Object.entries(menuItem.requires || {}); + + return ( + requiredPermissions.length === 0 || + requiredPermissions.every(([entityType, actions]) => + actions.every((action) => + hasPermission(entityType as EntityType, action), + ), + ) + ); +}; +/** + * Filters menu items based on user permissions. + * @param menuItems - The list of menu items to filter. + * @returns A filtered list of menu items that the user is allowed to access. + */ +const filterMenuItems = ( + menuItems: MenuItem[], + hasPermission: (entityType: EntityType, action: PermissionAction) => boolean, +): MenuItem[] => { + return menuItems + .map((menuItem) => { + // Validate top-level menu item without submenu + if ( + menuItem && + !menuItem.submenuItems && + isMenuItemAllowed(menuItem, hasPermission) + ) { + return menuItem; + } + + // Recursively process submenu items + if (menuItem.submenuItems) { + const filteredSubmenuItems = filterMenuItems( + menuItem.submenuItems, + hasPermission, + ); + + if (filteredSubmenuItems.length > 0) { + return { ...menuItem, submenuItems: filteredSubmenuItems }; + } + } + + return null; // Exclude invalid menu items + }) + .filter((menuItem): menuItem is MenuItem => !!menuItem); +}; +const useAvailableMenuItems = (menuItems: MenuItem[]): MenuItem[] => { + const hasPermission = useHasPermission(); + + return useMemo(() => { + return filterMenuItems(menuItems, hasPermission); + }, [menuItems, hasPermission]); +}; + +export default useAvailableMenuItems; diff --git a/frontend/src/hooks/useHasPermission.ts b/frontend/src/hooks/useHasPermission.ts index 09b6be68..0d9079c6 100644 --- a/frontend/src/hooks/useHasPermission.ts +++ b/frontend/src/hooks/useHasPermission.ts @@ -1,11 +1,12 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. * 2. All derivative works must include clear attribution to the original creator and software, Hexastack and Hexabot, in a prominent location (e.g., in the software's "About" section, documentation, and README file). */ + import { useCallback, useContext } from "react"; import { PermissionContext } from "@/contexts/permission.context"; @@ -18,7 +19,7 @@ export const useHasPermission = () => { (type: EntityType, action: PermissionAction) => { const allowedActions = getAllowedActions(type); - return allowedActions?.includes(action); + return !!allowedActions && allowedActions?.includes(action); }, [getAllowedActions], ); diff --git a/frontend/src/layout/VerticalMenu.tsx b/frontend/src/layout/VerticalMenu.tsx index d826a753..857f9dfa 100644 --- a/frontend/src/layout/VerticalMenu.tsx +++ b/frontend/src/layout/VerticalMenu.tsx @@ -1,5 +1,5 @@ /* - * Copyright © 2024 Hexastack. All rights reserved. + * Copyright © 2025 Hexastack. All rights reserved. * * Licensed under the GNU Affero General Public License v3.0 (AGPLv3) with the following additional terms: * 1. The name "Hexabot" is a trademark of Hexastack. You may not use this name in derivative works without express written permission. @@ -32,14 +32,13 @@ import { CSSObject, Grid, IconButton, styled, Theme } from "@mui/material"; import MuiDrawer from "@mui/material/Drawer"; import { OverridableComponent } from "@mui/material/OverridableComponent"; import { useRouter } from "next/router"; -import { FC, useMemo } from "react"; +import { FC } from "react"; import { HexabotLogo } from "@/app-components/logos/HexabotLogo"; import { Sidebar } from "@/app-components/menus/Sidebar"; import { useAuth } from "@/hooks/useAuth"; +import useAvailableMenuItems from "@/hooks/useAvailableMenuItems"; import { useConfig } from "@/hooks/useConfig"; -import { useHasPermission } from "@/hooks/useHasPermission"; -import { useTranslate } from "@/hooks/useTranslate"; import { EntityType } from "@/services/types"; import { PermissionAction } from "@/types/permission.types"; import { getLayout } from "@/utils/laylout"; @@ -206,7 +205,6 @@ const getMenuItems = (ssoEnabled: boolean): MenuItem[] => [ }, }, - // { // text: 'menu.broadcast', // href: "/subscribers/broadcast", @@ -287,25 +285,10 @@ export const VerticalMenu: FC = ({ onToggleOut, }) => { const { ssoEnabled } = useConfig(); - const { t } = useTranslate(); const { isAuthenticated } = useAuth(); const router = useRouter(); - const hasPermission = useHasPermission(); const menuItems = getMenuItems(ssoEnabled); - // Filter menu item to which user is allowed access - const availableMenuItems = useMemo(() => { - return menuItems.filter(({ requires: requiredPermissions }) => { - return ( - !requiredPermissions || - Object.entries(requiredPermissions).every((permission) => { - const entityType = permission[0] as EntityType; - const actions = permission[1]; - - return actions.every((action) => hasPermission(entityType, action)); - }) - ); - }); - }, [t, hasPermission]); + const availableMenuItems = useAvailableMenuItems(menuItems); const hasTemporaryDrawer = getLayout(router.pathname.slice(1)) === "full_width";