Added permission and role type safety

This commit is contained in:
sianida26 2024-03-29 00:54:26 +07:00
parent 56054ab408
commit a48e4353a5
14 changed files with 72 additions and 71 deletions

View File

@ -1,8 +1,5 @@
import { AppShell, AppShellHeader, Burger, MantineProvider } from "@mantine/core"; import { MantineProvider } from "@mantine/core";
import { useDisclosure } from "@mantine/hooks";
import Image from "next/image";
import React from "react"; import React from "react";
import logo from "@/assets/logos/logo.png";
import DashboardLayout from "@/modules/dashboard/components/DashboardLayout"; import DashboardLayout from "@/modules/dashboard/components/DashboardLayout";
import getUser from "@/modules/auth/actions/getMyDetailAction"; import getUser from "@/modules/auth/actions/getMyDetailAction";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";

View File

@ -2,12 +2,10 @@
import getMyDetail from "../services/getMyDetail"; import getMyDetail from "../services/getMyDetail";
import AuthError from "../error/AuthError"; import AuthError from "../error/AuthError";
import BaseError from "@/core/error/BaseError";
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
import handleCatch from "@/modules/dashboard/utils/handleCatch"; import handleCatch from "@/modules/dashboard/utils/handleCatch";
import "server-only"; import "server-only";
import { cookies } from "next/headers"; 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. * Asynchronously retrieves the authenticated user's details from a server-side context in a Next.js application.

View File

@ -12,5 +12,5 @@ import "server-only";
*/ */
export default async function logout() { export default async function logout() {
cookies().delete("token"); cookies().delete("token");
redirect("/login"); redirect("/dashboard/login");
} }

View File

@ -2,6 +2,7 @@ import getCurrentUser from "./getCurrentUser";
import "server-only"; import "server-only";
import getUserPermissions from "./getUserPermissions"; import getUserPermissions from "./getUserPermissions";
import { PermissionCode } from "@/modules/permission/data/initialPermissions"; import { PermissionCode } from "@/modules/permission/data/initialPermissions";
import AuthError from "../error/AuthError";
/** /**
* Deprecated. Use dashboard service instead * Deprecated. Use dashboard service instead
@ -12,9 +13,10 @@ import { PermissionCode } from "@/modules/permission/data/initialPermissions";
* @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" | "*" | PermissionCode | (string & {}), permission: PermissionCode,
currentUser?: Awaited<ReturnType<typeof getCurrentUser>> currentUser?: Awaited<ReturnType<typeof getCurrentUser>>
): Promise<boolean> { ): Promise<boolean> {
try {
// Allow if no specific permission is required. // Allow if no specific permission is required.
if (permission === "*") return true; if (permission === "*") return true;
@ -35,8 +37,14 @@ 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 = await getUserPermissions() const permissions = await getUserPermissions();
// Check if the user has the required permission. // Check if the user has the required permission.
return permissions.includes(permission); return permissions.includes(permission);
} catch (e) {
if (e instanceof AuthError && e.errorCode === "INVALID_JWT_TOKEN") {
return false;
}
throw e;
}
} }

View File

@ -1,4 +1,3 @@
import { cache } from "react"
import "server-only" import "server-only"
import getUserFromToken from "./getUserFromToken" import getUserFromToken from "./getUserFromToken"
import { cookies, headers } from "next/headers" import { cookies, headers } from "next/headers"

View File

@ -2,23 +2,26 @@ import "server-only";
import getCurrentUser from "./getCurrentUser"; import getCurrentUser from "./getCurrentUser";
import db from "@/core/db"; import db from "@/core/db";
import getUserRoles from "./getUserRoles"; import getUserRoles from "./getUserRoles";
import { PermissionCode } from "@/modules/permission/data/initialPermissions";
export default async function getUserPermissions() { export default async function getUserPermissions(): Promise<PermissionCode[]> {
const user = await getCurrentUser(); const user = await getCurrentUser();
if (!user) return []; 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")) { 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<string>([ const permissions = new Set<PermissionCode>([
...user.roles.flatMap((role) => ...user.roles.flatMap((role) =>
role.permissions.map((permission) => permission.code) role.permissions.map((permission) => permission.code)
), ),
...user.directPermissions.map((dp) => dp.code), ...user.directPermissions.map((dp) => dp.code),
]); ] as PermissionCode[]);
return Array.from(permissions); return Array.from(permissions);
} }

View File

@ -1,11 +1,12 @@
import { RoleCode } from "@/modules/permission/data/initialRoles";
import getCurrentUser from "./getCurrentUser"; import getCurrentUser from "./getCurrentUser";
export default async function getUserRoles() { export default async function getUserRoles(): Promise<RoleCode[]> {
const user = await getCurrentUser(); const user = await getCurrentUser();
if (!user) return []; if (!user) return [];
const roles = user?.roles.map((role) => role.code); const roles = user?.roles.map((role) => role.code);
return roles; return roles as RoleCode[];
} }

View File

@ -1,14 +1,6 @@
"use server"; "use server";
import {
TbLayoutDashboard,
TbUsers,
TbNotebook,
TbShoppingBag,
TbPhotoFilled,
} from "react-icons/tb";
import SidebarMenu from "../types/SidebarMenu"; import SidebarMenu from "../types/SidebarMenu";
import "server-only"; import "server-only";
import getCurrentUser from "@/modules/auth/utils/getCurrentUser";
import ServerResponseAction from "../types/ServerResponseAction"; import ServerResponseAction from "../types/ServerResponseAction";
import handleCatch from "../utils/handleCatch"; import handleCatch from "../utils/handleCatch";
import getUserRoles from "@/modules/auth/utils/getUserRoles"; import getUserRoles from "@/modules/auth/utils/getUserRoles";
@ -38,6 +30,7 @@ export default async function getSidebarMenus(): Promise<
) || ) ||
menuChild.allowedPermissions?.includes("*") || menuChild.allowedPermissions?.includes("*") ||
menuChild.allowedRoles?.includes("*") menuChild.allowedRoles?.includes("*")
|| roles.includes("super-admin")
) )
currentMenuChildren.push(menuChild); currentMenuChildren.push(menuChild);
} }

View File

@ -14,7 +14,7 @@ const userMenuItems: UserMenuItem[] = [
label: "Logout", label: "Logout",
icon: TbLogout, icon: TbLogout,
color: "red", color: "red",
href: "/logout", href: "/dashboard/logout",
}, },
]; ];

View File

@ -14,7 +14,7 @@ const sidebarMenus: SidebarMenu[] = [
{ {
label: "Users", label: "Users",
link: "/users", link: "/users",
allowedPermissions: ["users.getAll"], allowedPermissions: ["users.readAll"],
}, },
{ label: "Roles", link: "/roles", allowedRoles: ["super-admin"] }, { 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; export default sidebarMenus;

View File

@ -1,13 +1,16 @@
import { PermissionCode } from "@/modules/permission/data/initialPermissions";
import { RoleCode } from "@/modules/permission/data/initialRoles";
export default interface SidebarMenu { export default interface SidebarMenu {
label: string; label: string;
icon: React.FC<any> | string; icon: React.FC<any> | string;
children?: { children?: {
label: string; label: string;
link: string; link: string;
allowedPermissions?: string[], allowedPermissions?: PermissionCode[],
allowedRoles?: string[], allowedRoles?: RoleCode[],
}[]; }[];
color?: ThemeIconProps["color"]; color?: ThemeIconProps["color"];
allowedPermissions?: string[], allowedPermissions?: PermissionCode[],
allowedRoles?: string[] allowedRoles?: RoleCode[]
} }

View File

@ -84,7 +84,7 @@ const permissionData = [
} }
] as const; ] 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<Permission, "id">[]; const exportedPermissionData = permissionData as unknown as Omit<Permission, "id">[];

View File

@ -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<Role, "id">;
export default exportedRoleData;

View File

@ -9,7 +9,7 @@ const config: Config = {
theme: { theme: {
extend: {}, extend: {},
}, },
corePlugins: { preflight: false }, // corePlugins: { preflight: false },
plugins: [], plugins: [],
}; };
export default config; export default config;