Added dynamic sidebar
This commit is contained in:
parent
eb7dc05dc4
commit
09a9652d36
|
|
@ -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);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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: {
|
||||||
|
|
|
||||||
24
src/modules/auth/utils/getUserPermissions.ts
Normal file
24
src/modules/auth/utils/getUserPermissions.ts
Normal 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);
|
||||||
|
}
|
||||||
11
src/modules/auth/utils/getUserRoles.ts
Normal file
11
src/modules/auth/utils/getUserRoles.ts
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
6
src/modules/dashboard/types/SidebarMenu.d.ts
vendored
6
src/modules/dashboard/types/SidebarMenu.d.ts
vendored
|
|
@ -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[]
|
||||||
}
|
}
|
||||||
Loading…
Reference in New Issue
Block a user