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 "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<ReturnType<typeof getCurrentUser>>
): Promise<boolean> {
// 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<string>([
...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);
}

View File

@ -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: {

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";
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<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.
const userMenuItems: UserMenuItem[] = [
{
label: "Account Settings",
icon: TbSettings,
},
// {
// label: "Account Settings",
// icon: TbSettings,
// },
{
label: "Logout",
icon: TbLogout,

View File

@ -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<SidebarMenu[]>([]);
// Mapping all menu items to MenuItem components
const menus = getSidebarMenus().map((menu, i) => <MenuItem menu={menu} key={i} />);
// Mapping all menu items to MenuItem components
// 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 (
<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>
);
}

View File

@ -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) {
<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 (
<>
{/* Main Menu Item */}
@ -51,9 +57,7 @@ export default function MenuItem({ menu }: Props) {
{/* Icon and Label */}
<Box style={{ display: "flex", alignItems: "center" }}>
<ThemeIcon variant="light" size={30} color={menu.color}>
<menu.icon
style={{ width: rem(18), height: rem(18) }}
/>
<Icon style={{ width: rem(18), height: rem(18) }} />
</ThemeIcon>
<Box ml="md">{menu.label}</Box>

View File

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