From a48e4353a55f78988a310c5b45d817921894df99 Mon Sep 17 00:00:00 2001 From: sianida26 Date: Fri, 29 Mar 2024 00:54:26 +0700 Subject: [PATCH] Added permission and role type safety --- src/app/dashboard/layout.tsx | 5 +- src/modules/auth/actions/getMyDetailAction.ts | 2 - src/modules/auth/actions/logoutAction.ts | 2 +- src/modules/auth/utils/checkPermission.ts | 52 +++++++++++-------- src/modules/auth/utils/getCurrentUser.ts | 1 - src/modules/auth/utils/getUserPermissions.ts | 13 +++-- src/modules/auth/utils/getUserRoles.ts | 5 +- .../dashboard/actions/getSidebarMenus.ts | 9 +--- src/modules/dashboard/actions/getUserMenus.ts | 2 +- src/modules/dashboard/data/sidebarMenus.ts | 20 +------ src/modules/dashboard/types/SidebarMenu.d.ts | 11 ++-- .../permission/data/initialPermissions.ts | 2 +- src/modules/permission/data/initialRoles.ts | 17 ++++++ tailwind.config.ts | 2 +- 14 files changed, 72 insertions(+), 71 deletions(-) create mode 100644 src/modules/permission/data/initialRoles.ts diff --git a/src/app/dashboard/layout.tsx b/src/app/dashboard/layout.tsx index e8f1116..630afed 100644 --- a/src/app/dashboard/layout.tsx +++ b/src/app/dashboard/layout.tsx @@ -1,8 +1,5 @@ -import { AppShell, AppShellHeader, Burger, MantineProvider } from "@mantine/core"; -import { useDisclosure } from "@mantine/hooks"; -import Image from "next/image"; +import { MantineProvider } from "@mantine/core"; import React from "react"; -import logo from "@/assets/logos/logo.png"; import DashboardLayout from "@/modules/dashboard/components/DashboardLayout"; import getUser from "@/modules/auth/actions/getMyDetailAction"; import { redirect } from "next/navigation"; diff --git a/src/modules/auth/actions/getMyDetailAction.ts b/src/modules/auth/actions/getMyDetailAction.ts index 19213b1..dc3d65e 100644 --- a/src/modules/auth/actions/getMyDetailAction.ts +++ b/src/modules/auth/actions/getMyDetailAction.ts @@ -2,12 +2,10 @@ import getMyDetail from "../services/getMyDetail"; import AuthError from "../error/AuthError"; -import BaseError from "@/core/error/BaseError"; import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; import handleCatch from "@/modules/dashboard/utils/handleCatch"; import "server-only"; import { cookies } from "next/headers"; -import getUserFromToken from "../utils/getUserFromToken"; /** * Asynchronously retrieves the authenticated user's details from a server-side context in a Next.js application. diff --git a/src/modules/auth/actions/logoutAction.ts b/src/modules/auth/actions/logoutAction.ts index 96cd9f8..2206bae 100644 --- a/src/modules/auth/actions/logoutAction.ts +++ b/src/modules/auth/actions/logoutAction.ts @@ -12,5 +12,5 @@ import "server-only"; */ export default async function logout() { cookies().delete("token"); - redirect("/login"); + redirect("/dashboard/login"); } diff --git a/src/modules/auth/utils/checkPermission.ts b/src/modules/auth/utils/checkPermission.ts index 0cd0292..f0723d0 100644 --- a/src/modules/auth/utils/checkPermission.ts +++ b/src/modules/auth/utils/checkPermission.ts @@ -2,6 +2,7 @@ import getCurrentUser from "./getCurrentUser"; import "server-only"; import getUserPermissions from "./getUserPermissions"; import { PermissionCode } from "@/modules/permission/data/initialPermissions"; +import AuthError from "../error/AuthError"; /** * Deprecated. Use dashboard service instead @@ -12,31 +13,38 @@ import { PermissionCode } from "@/modules/permission/data/initialPermissions"; * @returns true if the user has the required permission, otherwise false. */ export default async function checkPermission( - permission: "guest-only" | "authenticated-only" | "*" | PermissionCode | (string & {}), + permission: PermissionCode, currentUser?: Awaited> ): Promise { - // Allow if no specific permission is required. - if (permission === "*") return true; + try { + // Allow if no specific permission is required. + if (permission === "*") return true; - // Retrieve current user if not provided. - const user = currentUser ?? (await getCurrentUser()); + // Retrieve current user if not provided. + const user = currentUser ?? (await getCurrentUser()); - // Handle non-authenticated users. - if (!user) { - return permission === "guest-only"; + // Handle non-authenticated users. + if (!user) { + return permission === "guest-only"; + } + + // Allow authenticated users if the permission is 'authenticated-only'. + if (permission === "authenticated-only") { + return true; + } + + // Short-circuit for super-admin role to allow all permissions. + 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 = await getUserPermissions(); + + // Check if the user has the required permission. + return permissions.includes(permission); + } catch (e) { + if (e instanceof AuthError && e.errorCode === "INVALID_JWT_TOKEN") { + return false; + } + throw e; } - - // Allow authenticated users if the permission is 'authenticated-only'. - if (permission === "authenticated-only") { - return true; - } - - // Short-circuit for super-admin role to allow all permissions. - 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 = await getUserPermissions() - - // Check if the user has the required permission. - return permissions.includes(permission); } diff --git a/src/modules/auth/utils/getCurrentUser.ts b/src/modules/auth/utils/getCurrentUser.ts index 1787e02..3824f8f 100644 --- a/src/modules/auth/utils/getCurrentUser.ts +++ b/src/modules/auth/utils/getCurrentUser.ts @@ -1,4 +1,3 @@ -import { cache } from "react" import "server-only" import getUserFromToken from "./getUserFromToken" import { cookies, headers } from "next/headers" diff --git a/src/modules/auth/utils/getUserPermissions.ts b/src/modules/auth/utils/getUserPermissions.ts index 5c1bcd6..b72986b 100644 --- a/src/modules/auth/utils/getUserPermissions.ts +++ b/src/modules/auth/utils/getUserPermissions.ts @@ -2,23 +2,26 @@ import "server-only"; import getCurrentUser from "./getCurrentUser"; import db from "@/core/db"; import getUserRoles from "./getUserRoles"; +import { PermissionCode } from "@/modules/permission/data/initialPermissions"; -export default async function getUserPermissions() { +export default async function getUserPermissions(): Promise { const user = await getCurrentUser(); if (!user) return []; - //Retrieve all permissions if the user is super admin + //Retrieve all permissions if the user is super admin if ((await getUserRoles()).includes("super-admin")) { - return (await db.permission.findMany()).map((permission) => permission.code); + return (await db.permission.findMany()).map( + (permission) => permission.code + ) as PermissionCode[]; } - const permissions = new Set([ + const permissions = new Set([ ...user.roles.flatMap((role) => role.permissions.map((permission) => permission.code) ), ...user.directPermissions.map((dp) => dp.code), - ]); + ] as PermissionCode[]); return Array.from(permissions); } diff --git a/src/modules/auth/utils/getUserRoles.ts b/src/modules/auth/utils/getUserRoles.ts index bb37202..838e597 100644 --- a/src/modules/auth/utils/getUserRoles.ts +++ b/src/modules/auth/utils/getUserRoles.ts @@ -1,11 +1,12 @@ +import { RoleCode } from "@/modules/permission/data/initialRoles"; import getCurrentUser from "./getCurrentUser"; -export default async function getUserRoles() { +export default async function getUserRoles(): Promise { const user = await getCurrentUser(); if (!user) return []; const roles = user?.roles.map((role) => role.code); - return roles; + return roles as RoleCode[]; } diff --git a/src/modules/dashboard/actions/getSidebarMenus.ts b/src/modules/dashboard/actions/getSidebarMenus.ts index 040ef63..cb29e95 100644 --- a/src/modules/dashboard/actions/getSidebarMenus.ts +++ b/src/modules/dashboard/actions/getSidebarMenus.ts @@ -1,14 +1,6 @@ "use server"; -import { - TbLayoutDashboard, - TbUsers, - TbNotebook, - TbShoppingBag, - 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"; @@ -38,6 +30,7 @@ export default async function getSidebarMenus(): Promise< ) || menuChild.allowedPermissions?.includes("*") || menuChild.allowedRoles?.includes("*") + || roles.includes("super-admin") ) currentMenuChildren.push(menuChild); } diff --git a/src/modules/dashboard/actions/getUserMenus.ts b/src/modules/dashboard/actions/getUserMenus.ts index 6538840..9922033 100644 --- a/src/modules/dashboard/actions/getUserMenus.ts +++ b/src/modules/dashboard/actions/getUserMenus.ts @@ -14,7 +14,7 @@ const userMenuItems: UserMenuItem[] = [ label: "Logout", icon: TbLogout, color: "red", - href: "/logout", + href: "/dashboard/logout", }, ]; diff --git a/src/modules/dashboard/data/sidebarMenus.ts b/src/modules/dashboard/data/sidebarMenus.ts index 682fd4e..9697f76 100644 --- a/src/modules/dashboard/data/sidebarMenus.ts +++ b/src/modules/dashboard/data/sidebarMenus.ts @@ -14,7 +14,7 @@ const sidebarMenus: SidebarMenu[] = [ { label: "Users", link: "/users", - allowedPermissions: ["users.getAll"], + allowedPermissions: ["users.readAll"], }, { label: "Roles", link: "/roles", allowedRoles: ["super-admin"] }, { @@ -24,24 +24,6 @@ const sidebarMenus: SidebarMenu[] = [ }, ], }, - { - label: "Reseller Office 365", - icon: "TbBuildingStore", - color: "red", - allowedPermissions: ["*"], - children: [ - { - label: "My Request Links", - link: "/reseller-office-365/request", - allowedRoles: ["reseller-office-365"] - }, - { - label: "Process Request Link", - link: "/reseller-office-365/list", - allowedRoles: ["admin-reseller-office-365"] - } - ] - } ]; export default sidebarMenus; diff --git a/src/modules/dashboard/types/SidebarMenu.d.ts b/src/modules/dashboard/types/SidebarMenu.d.ts index 3267e9d..b41742f 100644 --- a/src/modules/dashboard/types/SidebarMenu.d.ts +++ b/src/modules/dashboard/types/SidebarMenu.d.ts @@ -1,13 +1,16 @@ +import { PermissionCode } from "@/modules/permission/data/initialPermissions"; +import { RoleCode } from "@/modules/permission/data/initialRoles"; + export default interface SidebarMenu { label: string; icon: React.FC | string; children?: { label: string; link: string; - allowedPermissions?: string[], - allowedRoles?: string[], + allowedPermissions?: PermissionCode[], + allowedRoles?: RoleCode[], }[]; color?: ThemeIconProps["color"]; - allowedPermissions?: string[], - allowedRoles?: string[] + allowedPermissions?: PermissionCode[], + allowedRoles?: RoleCode[] } \ No newline at end of file diff --git a/src/modules/permission/data/initialPermissions.ts b/src/modules/permission/data/initialPermissions.ts index 179d964..9d4391e 100644 --- a/src/modules/permission/data/initialPermissions.ts +++ b/src/modules/permission/data/initialPermissions.ts @@ -84,7 +84,7 @@ const permissionData = [ } ] as const; -export type PermissionCode = (typeof permissionData)[number]['code']; +export type PermissionCode = (typeof permissionData)[number]['code'] | "*" | "authenticated-only" | "guest-only"; const exportedPermissionData = permissionData as unknown as Omit[]; diff --git a/src/modules/permission/data/initialRoles.ts b/src/modules/permission/data/initialRoles.ts new file mode 100644 index 0000000..27d7dda --- /dev/null +++ b/src/modules/permission/data/initialRoles.ts @@ -0,0 +1,17 @@ +import { Role } from "@prisma/client"; + +const roleData = [ + { + code: "super-admin", + description: + "Has full access to the system and can manage all features and settings", + isActive: true, + name: "Super Admin", + }, +] as const; + +export type RoleCode = (typeof roleData)[number]["code"] | "*"; + +const exportedRoleData = roleData as unknown as Omit; + +export default exportedRoleData; diff --git a/tailwind.config.ts b/tailwind.config.ts index 4705918..8ebf6a1 100644 --- a/tailwind.config.ts +++ b/tailwind.config.ts @@ -9,7 +9,7 @@ const config: Config = { theme: { extend: {}, }, - corePlugins: { preflight: false }, + // corePlugins: { preflight: false }, plugins: [], }; export default config;