Added dynamic sidebar

This commit is contained in:
sianida26 2024-02-14 23:22:00 +07:00
parent eb7dc05dc4
commit 09a9652d36
9 changed files with 182 additions and 37 deletions

View File

@ -1,5 +1,6 @@
import getCurrentUser from "./getCurrentUser"; import getCurrentUser from "./getCurrentUser";
import "server-only"; import "server-only";
import getUserPermissions from "./getUserPermissions";
/** /**
* Deprecated. Use dashboard service instead * Deprecated. Use dashboard service instead
@ -11,11 +12,11 @@ import "server-only";
* @returns true if the user has the required permission, otherwise false. * @returns true if the user has the required permission, otherwise false.
*/ */
export default async function checkPermission( export default async function checkPermission(
permission?: "guest-only" | "authenticated-only" | (string & {}), permission: "guest-only" | "authenticated-only" | "*" | (string & {}),
currentUser?: Awaited<ReturnType<typeof getCurrentUser>> currentUser?: Awaited<ReturnType<typeof getCurrentUser>>
): Promise<boolean> { ): Promise<boolean> {
// Allow if no specific permission is required. // Allow if no specific permission is required.
if (!permission) return true; if (permission === "*") return true;
// Retrieve current user if not provided. // Retrieve current user if not provided.
const user = currentUser ?? (await getCurrentUser()); 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; if (user.roles.some((role) => role.code === "super-admin")) return true;
// Aggregate all role codes and direct permissions into a set for efficient lookup. // Aggregate all role codes and direct permissions into a set for efficient lookup.
const permissions = new Set<string>([ const permissions = await getUserPermissions()
...user.roles.map((role) => role.code),
...user.directPermissions.map((dp) => dp.code),
]);
// Check if the user has the required permission. // Check if the user has the required permission.
return permissions.has(permission); return permissions.includes(permission);
} }

View File

@ -19,7 +19,11 @@ const getUserFromToken = cache(async (token: string) => {
// Fetch the user from the database // Fetch the user from the database
const user = await prisma.user.findFirst({ const user = await prisma.user.findFirst({
include: { include: {
roles: true, roles: {
include: {
permissions: true
}
},
directPermissions: true, directPermissions: true,
}, },
where: { where: {

View File

@ -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<string>([
...user.roles.flatMap((role) =>
role.permissions.map((permission) => permission.code)
),
...user.directPermissions.map((dp) => dp.code),
]);
return Array.from(permissions);
}

View File

@ -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;
}

View File

@ -1,5 +1,4 @@
import { ThemeIconProps } from "@mantine/core"; "use server";
import React from "react";
import { import {
TbLayoutDashboard, TbLayoutDashboard,
TbUsers, TbUsers,
@ -8,25 +7,40 @@ import {
TbPhotoFilled, TbPhotoFilled,
} from "react-icons/tb"; } from "react-icons/tb";
import SidebarMenu from "../types/SidebarMenu"; 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[] = [ const sidebarMenus: SidebarMenu[] = [
{ {
label: "Dashboard", label: "Dashboard",
icon: TbLayoutDashboard, icon: "TbLayoutDashboard",
allowedPermissions: ["*"],
}, },
{ {
label: "Users", label: "Users",
icon: TbUsers, icon: "TbUsers",
color: "grape", color: "grape",
children: [ children: [
{ label: "Users", link: "/users" }, {
{ label: "Roles", link: "/roles" }, label: "Users",
{ label: "Permissions", link: "/permissions" }, link: "/users",
allowedPermissions: ["users.getAll"],
},
{ label: "Roles", link: "/roles", allowedRoles: ["super-admin"] },
{
label: "Permissions",
link: "/permissions",
allowedRoles: ["super-admin"],
},
], ],
}, },
{ {
label: "Blog", label: "Blog",
icon: TbNotebook, icon: "TbNotebook",
color: "green", color: "green",
children: [ children: [
{ label: "Posts", link: "#" }, { label: "Posts", link: "#" },
@ -36,17 +50,78 @@ const sidebarMenus: SidebarMenu[] = [
}, },
{ {
label: "Products", label: "Products",
icon: TbShoppingBag, icon: "TbShoppingBag",
color: "cyan", color: "cyan",
}, },
{ {
label: "Banners", label: "Banners",
icon: TbPhotoFilled, icon: "TbPhotoFilled",
color: "indigo", color: "indigo",
}, },
]; ];
//TODO: Change into server actions export default async function getSidebarMenus(): Promise<
const getSidebarMenus = () => sidebarMenus; ServerResponseAction<SidebarMenu[]>
> {
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);
}
}

View File

@ -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. // This function retrieves the list of user menu items for use in the application's header.
const userMenuItems: UserMenuItem[] = [ const userMenuItems: UserMenuItem[] = [
{ // {
label: "Account Settings", // label: "Account Settings",
icon: TbSettings, // icon: TbSettings,
}, // },
{ {
label: "Logout", label: "Logout",
icon: TbLogout, icon: TbLogout,

View File

@ -1,8 +1,10 @@
import React from 'react'; import React, { useEffect, useState } from "react";
import { AppShell, ScrollArea } from '@mantine/core'; import { AppShell, ScrollArea, Skeleton, Stack } from "@mantine/core";
import MenuItem from './SidebarMenuItem'; import MenuItem from "./SidebarMenuItem";
import getSidebarMenus from '../actions/getSidebarMenus'; 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. * `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. * @returns A React element representing the application's navigation bar.
*/ */
export default function AppNavbar() { export default function AppNavbar() {
const [isFetching, setFetching] = useState(true);
const [sidebarMenus, setSidebarMenus] = useState<SidebarMenu[]>([]);
// Mapping all menu items to MenuItem components // Mapping all menu items to MenuItem components
const menus = getSidebarMenus().map((menu, i) => <MenuItem menu={menu} key={i} />); // const menus = getSidebarMenus().map((menu, i) => <MenuItem menu={menu} key={i} />);
useEffect(() => {
setFetching(true);
withServerAction(getSidebarMenus)
.then((response) => {
setSidebarMenus(response.data);
})
.catch((e) => {
console.error(e);
})
.finally(() => {
setFetching(false);
});
}, []);
return ( return (
<AppShell.Navbar p="md"> <AppShell.Navbar p="md">
<ScrollArea style={{ flex: "1" }}>{menus}</ScrollArea> <ScrollArea style={{ flex: "1" }}>
{
isFetching ? <Stack gap="md">
{[...new Array(10)].map((_,i) => <Skeleton key={i} visible={true} height={40} width={"100%"} />)}
</Stack> :
sidebarMenus.map((menu, i) => (
<MenuItem menu={menu} key={i} />
))}
</ScrollArea>
</AppShell.Navbar> </AppShell.Navbar>
); );
} }

View File

@ -1,4 +1,4 @@
import React, { useState } from "react"; import React, { ReactNode, useState } from "react";
import { import {
Box, Box,
@ -9,6 +9,7 @@ import {
rem, rem,
} from "@mantine/core"; } from "@mantine/core";
import { TbChevronRight } from "react-icons/tb"; import { TbChevronRight } from "react-icons/tb";
import * as TbIcons from "react-icons/tb";
import ChildMenu from "./SidebarChildMenu"; import ChildMenu from "./SidebarChildMenu";
import classNames from "./styles/sidebarMenuItem.module.css"; import classNames from "./styles/sidebarMenuItem.module.css";
@ -40,6 +41,11 @@ export default function MenuItem({ menu }: Props) {
<ChildMenu key={index} item={child} /> <ChildMenu key={index} item={child} />
)); ));
const Icons = TbIcons as any;
const Icon = typeof menu.icon === "string" ? Icons[menu.icon] : menu.icon;
// const a = typeof menu.icon === "string"
return ( return (
<> <>
{/* Main Menu Item */} {/* Main Menu Item */}
@ -51,9 +57,7 @@ export default function MenuItem({ menu }: Props) {
{/* Icon and Label */} {/* Icon and Label */}
<Box style={{ display: "flex", alignItems: "center" }}> <Box style={{ display: "flex", alignItems: "center" }}>
<ThemeIcon variant="light" size={30} color={menu.color}> <ThemeIcon variant="light" size={30} color={menu.color}>
<menu.icon <Icon style={{ width: rem(18), height: rem(18) }} />
style={{ width: rem(18), height: rem(18) }}
/>
</ThemeIcon> </ThemeIcon>
<Box ml="md">{menu.label}</Box> <Box ml="md">{menu.label}</Box>

View File

@ -1,9 +1,13 @@
export default interface SidebarMenu { export default interface SidebarMenu {
label: string; label: string;
icon: React.FC<any>; icon: React.FC<any> | string;
children?: { children?: {
label: string; label: string;
link: string; link: string;
allowedPermissions?: string[],
allowedRoles?: string[],
}[]; }[];
color?: ThemeIconProps["color"]; color?: ThemeIconProps["color"];
allowedPermissions?: string[],
allowedRoles?: string[]
} }