From 09a9652d367eb87d451ff6f3a35c58570a9b0a66 Mon Sep 17 00:00:00 2001 From: sianida26 Date: Wed, 14 Feb 2024 23:22:00 +0700 Subject: [PATCH] Added dynamic sidebar --- src/modules/auth/utils/checkPermission.ts | 12 +-- src/modules/auth/utils/getUserFromToken.ts | 6 +- src/modules/auth/utils/getUserPermissions.ts | 24 +++++ src/modules/auth/utils/getUserRoles.ts | 11 ++ .../dashboard/actions/getSidebarMenus.ts | 101 +++++++++++++++--- src/modules/dashboard/actions/getUserMenus.ts | 8 +- .../dashboard/components/AppNavbar.tsx | 39 +++++-- .../dashboard/components/SidebarMenuItem.tsx | 12 ++- src/modules/dashboard/types/SidebarMenu.d.ts | 6 +- 9 files changed, 182 insertions(+), 37 deletions(-) create mode 100644 src/modules/auth/utils/getUserPermissions.ts create mode 100644 src/modules/auth/utils/getUserRoles.ts diff --git a/src/modules/auth/utils/checkPermission.ts b/src/modules/auth/utils/checkPermission.ts index 74c50b2..35727c3 100644 --- a/src/modules/auth/utils/checkPermission.ts +++ b/src/modules/auth/utils/checkPermission.ts @@ -1,5 +1,6 @@ import getCurrentUser from "./getCurrentUser"; import "server-only"; +import getUserPermissions from "./getUserPermissions"; /** * Deprecated. Use dashboard service instead @@ -11,11 +12,11 @@ import "server-only"; * @returns true if the user has the required permission, otherwise false. */ export default async function checkPermission( - permission?: "guest-only" | "authenticated-only" | (string & {}), + permission: "guest-only" | "authenticated-only" | "*" | (string & {}), currentUser?: Awaited> ): Promise { // Allow if no specific permission is required. - if (!permission) return true; + if (permission === "*") return true; // Retrieve current user if not provided. const user = currentUser ?? (await getCurrentUser()); @@ -34,11 +35,8 @@ export default async function checkPermission( if (user.roles.some((role) => role.code === "super-admin")) return true; // Aggregate all role codes and direct permissions into a set for efficient lookup. - const permissions = new Set([ - ...user.roles.map((role) => role.code), - ...user.directPermissions.map((dp) => dp.code), - ]); + const permissions = await getUserPermissions() // Check if the user has the required permission. - return permissions.has(permission); + return permissions.includes(permission); } diff --git a/src/modules/auth/utils/getUserFromToken.ts b/src/modules/auth/utils/getUserFromToken.ts index d44f55e..a4011ff 100644 --- a/src/modules/auth/utils/getUserFromToken.ts +++ b/src/modules/auth/utils/getUserFromToken.ts @@ -19,7 +19,11 @@ const getUserFromToken = cache(async (token: string) => { // Fetch the user from the database const user = await prisma.user.findFirst({ include: { - roles: true, + roles: { + include: { + permissions: true + } + }, directPermissions: true, }, where: { diff --git a/src/modules/auth/utils/getUserPermissions.ts b/src/modules/auth/utils/getUserPermissions.ts new file mode 100644 index 0000000..5c1bcd6 --- /dev/null +++ b/src/modules/auth/utils/getUserPermissions.ts @@ -0,0 +1,24 @@ +import "server-only"; +import getCurrentUser from "./getCurrentUser"; +import db from "@/core/db"; +import getUserRoles from "./getUserRoles"; + +export default async function getUserPermissions() { + const user = await getCurrentUser(); + + if (!user) return []; + + //Retrieve all permissions if the user is super admin + if ((await getUserRoles()).includes("super-admin")) { + return (await db.permission.findMany()).map((permission) => permission.code); + } + + const permissions = new Set([ + ...user.roles.flatMap((role) => + role.permissions.map((permission) => permission.code) + ), + ...user.directPermissions.map((dp) => dp.code), + ]); + + return Array.from(permissions); +} diff --git a/src/modules/auth/utils/getUserRoles.ts b/src/modules/auth/utils/getUserRoles.ts new file mode 100644 index 0000000..bb37202 --- /dev/null +++ b/src/modules/auth/utils/getUserRoles.ts @@ -0,0 +1,11 @@ +import getCurrentUser from "./getCurrentUser"; + +export default async function getUserRoles() { + const user = await getCurrentUser(); + + if (!user) return []; + + const roles = user?.roles.map((role) => role.code); + + return roles; +} diff --git a/src/modules/dashboard/actions/getSidebarMenus.ts b/src/modules/dashboard/actions/getSidebarMenus.ts index c6dc717..a6f86d9 100644 --- a/src/modules/dashboard/actions/getSidebarMenus.ts +++ b/src/modules/dashboard/actions/getSidebarMenus.ts @@ -1,5 +1,4 @@ -import { ThemeIconProps } from "@mantine/core"; -import React from "react"; +"use server"; import { TbLayoutDashboard, TbUsers, @@ -8,25 +7,40 @@ import { TbPhotoFilled, } from "react-icons/tb"; import SidebarMenu from "../types/SidebarMenu"; +import "server-only"; +import getCurrentUser from "@/modules/auth/utils/getCurrentUser"; +import ServerResponseAction from "../types/ServerResponseAction"; +import handleCatch from "../utils/handleCatch"; +import getUserRoles from "@/modules/auth/utils/getUserRoles"; +import getUserPermissions from "@/modules/auth/utils/getUserPermissions"; const sidebarMenus: SidebarMenu[] = [ { label: "Dashboard", - icon: TbLayoutDashboard, + icon: "TbLayoutDashboard", + allowedPermissions: ["*"], }, { label: "Users", - icon: TbUsers, + icon: "TbUsers", color: "grape", children: [ - { label: "Users", link: "/users" }, - { label: "Roles", link: "/roles" }, - { label: "Permissions", link: "/permissions" }, + { + label: "Users", + link: "/users", + allowedPermissions: ["users.getAll"], + }, + { label: "Roles", link: "/roles", allowedRoles: ["super-admin"] }, + { + label: "Permissions", + link: "/permissions", + allowedRoles: ["super-admin"], + }, ], }, { label: "Blog", - icon: TbNotebook, + icon: "TbNotebook", color: "green", children: [ { label: "Posts", link: "#" }, @@ -36,17 +50,78 @@ const sidebarMenus: SidebarMenu[] = [ }, { label: "Products", - icon: TbShoppingBag, + icon: "TbShoppingBag", color: "cyan", }, { label: "Banners", - icon: TbPhotoFilled, + icon: "TbPhotoFilled", color: "indigo", }, ]; -//TODO: Change into server actions -const getSidebarMenus = () => sidebarMenus; +export default async function getSidebarMenus(): Promise< + ServerResponseAction +> { + try { + const filteredMenus: SidebarMenu[] = []; -export default getSidebarMenus; + const roles = await getUserRoles(); + const permissions = await getUserPermissions(); + + for (let menu of sidebarMenus) { + console.log("aaa"); + //if has children + if (menu.children) { + const currentMenuChildren: SidebarMenu["children"] = []; + for (let menuChild of menu.children) { + if ( + menuChild.allowedPermissions?.some((perm) => + permissions?.includes(perm) + ) || + menuChild.allowedRoles?.some((role) => + roles?.includes(role) + ) || + menuChild.allowedPermissions?.includes("*") || + menuChild.allowedRoles?.includes("*") + ) + currentMenuChildren.push(menuChild); + } + + if (currentMenuChildren.length > 0) { + filteredMenus.push({ + ...menu, + children: currentMenuChildren, + }); + } + } + //if does not have any children + else { + // console.table({ + // allowedPermissions: menu.allowedPermissions, + // userPermissions: permissions + // }) + if ( + menu.allowedPermissions?.some((perm) => + permissions?.includes(perm) + ) || + menu.allowedRoles?.some((role) => roles?.includes(role)) || + menu.allowedPermissions?.includes("*") || + menu.allowedRoles?.includes("*") + ) { + filteredMenus.push(menu); + } + } + } + + console.log("permissions", permissions); + console.log("menus", filteredMenus); + + return { + success: true, + data: filteredMenus, + }; + } catch (e) { + return handleCatch(e); + } +} diff --git a/src/modules/dashboard/actions/getUserMenus.ts b/src/modules/dashboard/actions/getUserMenus.ts index fdc649e..6538840 100644 --- a/src/modules/dashboard/actions/getUserMenus.ts +++ b/src/modules/dashboard/actions/getUserMenus.ts @@ -6,10 +6,10 @@ import { UserMenuItem } from "../types/UserMenuItem"; // This function retrieves the list of user menu items for use in the application's header. const userMenuItems: UserMenuItem[] = [ - { - label: "Account Settings", - icon: TbSettings, - }, + // { + // label: "Account Settings", + // icon: TbSettings, + // }, { label: "Logout", icon: TbLogout, diff --git a/src/modules/dashboard/components/AppNavbar.tsx b/src/modules/dashboard/components/AppNavbar.tsx index adf1577..e071c76 100644 --- a/src/modules/dashboard/components/AppNavbar.tsx +++ b/src/modules/dashboard/components/AppNavbar.tsx @@ -1,8 +1,10 @@ -import React from 'react'; -import { AppShell, ScrollArea } from '@mantine/core'; +import React, { useEffect, useState } from "react"; +import { AppShell, ScrollArea, Skeleton, Stack } from "@mantine/core"; -import MenuItem from './SidebarMenuItem'; -import getSidebarMenus from '../actions/getSidebarMenus'; +import MenuItem from "./SidebarMenuItem"; +import getSidebarMenus from "../actions/getSidebarMenus"; +import withServerAction from "../utils/withServerAction"; +import SidebarMenu from "../types/SidebarMenu"; /** * `AppNavbar` is a React functional component that renders the application's navigation bar. @@ -11,13 +13,36 @@ import getSidebarMenus from '../actions/getSidebarMenus'; * @returns A React element representing the application's navigation bar. */ export default function AppNavbar() { + const [isFetching, setFetching] = useState(true); + const [sidebarMenus, setSidebarMenus] = useState([]); - // Mapping all menu items to MenuItem components - const menus = getSidebarMenus().map((menu, i) => ); + // Mapping all menu items to MenuItem components + // const menus = getSidebarMenus().map((menu, i) => ); + useEffect(() => { + setFetching(true); + withServerAction(getSidebarMenus) + .then((response) => { + setSidebarMenus(response.data); + }) + .catch((e) => { + console.error(e); + }) + .finally(() => { + setFetching(false); + }); + }, []); return ( - {menus} + + { + isFetching ? + {[...new Array(10)].map((_,i) => )} + : + sidebarMenus.map((menu, i) => ( + + ))} + ); } diff --git a/src/modules/dashboard/components/SidebarMenuItem.tsx b/src/modules/dashboard/components/SidebarMenuItem.tsx index e581d26..7d0cecc 100644 --- a/src/modules/dashboard/components/SidebarMenuItem.tsx +++ b/src/modules/dashboard/components/SidebarMenuItem.tsx @@ -1,4 +1,4 @@ -import React, { useState } from "react"; +import React, { ReactNode, useState } from "react"; import { Box, @@ -9,6 +9,7 @@ import { rem, } from "@mantine/core"; import { TbChevronRight } from "react-icons/tb"; +import * as TbIcons from "react-icons/tb"; import ChildMenu from "./SidebarChildMenu"; import classNames from "./styles/sidebarMenuItem.module.css"; @@ -40,6 +41,11 @@ export default function MenuItem({ menu }: Props) { )); + const Icons = TbIcons as any; + + const Icon = typeof menu.icon === "string" ? Icons[menu.icon] : menu.icon; + // const a = typeof menu.icon === "string" + return ( <> {/* Main Menu Item */} @@ -51,9 +57,7 @@ export default function MenuItem({ menu }: Props) { {/* Icon and Label */} - + {menu.label} diff --git a/src/modules/dashboard/types/SidebarMenu.d.ts b/src/modules/dashboard/types/SidebarMenu.d.ts index 8603520..3267e9d 100644 --- a/src/modules/dashboard/types/SidebarMenu.d.ts +++ b/src/modules/dashboard/types/SidebarMenu.d.ts @@ -1,9 +1,13 @@ export default interface SidebarMenu { label: string; - icon: React.FC; + icon: React.FC | string; children?: { label: string; link: string; + allowedPermissions?: string[], + allowedRoles?: string[], }[]; color?: ThemeIconProps["color"]; + allowedPermissions?: string[], + allowedRoles?: string[] } \ No newline at end of file