From 85daf8cfe7b6a65e9017d609f8d3a3ad3ff410d9 Mon Sep 17 00:00:00 2001 From: Sianida26 Date: Sun, 28 Jan 2024 00:04:45 +0700 Subject: [PATCH] Update role columns --- .../roles/_tables/RolesTable/RolesTable.tsx | 10 +- .../roles/_tables/RolesTable/columns.tsx | 124 +++++++++++++----- src/app/dashboard/(auth)/roles/page.tsx | 19 ++- .../auth/tools/checkMultiplePermissions.ts | 31 +++++ .../dashboard/roles/actions/upsertRole.ts | 74 ++++++----- src/features/dashboard/roles/data/getRoles.ts | 45 +++++++ .../dashboard/utils/createActionButtons.tsx | 50 +++++++ 7 files changed, 277 insertions(+), 76 deletions(-) create mode 100644 src/features/auth/tools/checkMultiplePermissions.ts create mode 100644 src/features/dashboard/roles/data/getRoles.ts create mode 100644 src/features/dashboard/utils/createActionButtons.tsx diff --git a/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx b/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx index 595ed8d..e0e37c5 100644 --- a/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx +++ b/src/app/dashboard/(auth)/roles/_tables/RolesTable/RolesTable.tsx @@ -6,17 +6,21 @@ import { useReactTable, } from "@tanstack/react-table"; import React from "react"; -import columns from "./columns"; import CrudPermissions from "@/features/auth/types/CrudPermissions"; +import getRoles from "@/features/dashboard/roles/data/getRoles"; +import createColumns from "./columns"; interface Props { permissions: Partial, + roles: Awaited> } export default function RolesTable(props: Props) { const table = useReactTable({ - data: [], - columns, + data: props.roles, + columns: createColumns({ + permissions: props.permissions + }), getCoreRowModel: getCoreRowModel(), defaultColumn: { cell: (props) => {props.getValue() as React.ReactNode}, diff --git a/src/app/dashboard/(auth)/roles/_tables/RolesTable/columns.tsx b/src/app/dashboard/(auth)/roles/_tables/RolesTable/columns.tsx index 9789758..3589eee 100644 --- a/src/app/dashboard/(auth)/roles/_tables/RolesTable/columns.tsx +++ b/src/app/dashboard/(auth)/roles/_tables/RolesTable/columns.tsx @@ -1,43 +1,105 @@ +import CrudPermissions from "@/features/auth/types/CrudPermissions"; +import createActionButtons from "@/features/dashboard/utils/createActionButtons"; +import { Badge, Flex, Tooltip, ActionIcon } from "@mantine/core"; import { createColumnHelper } from "@tanstack/react-table"; +import Link from "next/link"; import { StringifyOptions } from "querystring"; +import { TbEye, TbPencil, TbTrash } from "react-icons/tb"; export interface RoleRow { - id: string, - code: string, - name: string, - permissionCount: number, - userCount: number, + id: string; + code: string; + name: string; + isActive: boolean; + permissionCount: number; + userCount: number; } -const columnHelper = createColumnHelper() +interface ColumnOptions { + permissions: Partial; + actions?: { + detail?: () => void; + edit?: () => void; + delete?: () => void; + }; +} -const columns = [ - columnHelper.accessor("id",{ - id: "sequence", - header: "#", - cell: props => props.row.index + 1, - }), - - columnHelper.accessor("code", { - header: 'Code', - }), +const createColumns = (options: ColumnOptions) => { + const columnHelper = createColumnHelper(); - columnHelper.accessor("name", { - header: "Name" - }), + const columns = [ + columnHelper.accessor("id", { + id: "sequence", + header: "#", + cell: (props) => props.row.index + 1, + }), - columnHelper.accessor("permissionCount", { - header: "Permissions" - }), + columnHelper.accessor("code", { + header: "Code", + }), - columnHelper.accessor("userCount", { - header: "Users" - }), + columnHelper.accessor("name", { + header: "Name", + }), - columnHelper.display({ - id: "Actions", - header: "Actions", - }) -] + columnHelper.accessor("isActive", { + header: "Status", + cell: (props) => + props.getValue() ? ( + Enabled + ) : ( + Disabled + ), + }), -export default columns; + columnHelper.accessor("permissionCount", { + header: "Permissions", + }), + + columnHelper.accessor("userCount", { + header: "Users", + }), + + columnHelper.display({ + id: "Actions", + header: "Actions", + size: 10, + meta: { + className: "w-fit", + }, + cell: () => ( + + { + createActionButtons([ + { + label: "Detail", + permission: options.permissions.read, + action: options.actions?.detail, + color: "green", + icon: + }, + { + label: "Edit", + permission: options.permissions.update, + action: options.actions?.edit, + color: "yellow", + icon: + }, + { + label: "Delete", + permission: options.permissions.delete, + action: options.actions?.delete, + color: "red", + icon: + } + ]) + } + + ), + }), + ]; + + return columns; +}; + +export default createColumns; diff --git a/src/app/dashboard/(auth)/roles/page.tsx b/src/app/dashboard/(auth)/roles/page.tsx index f4ba1dc..8a50be8 100644 --- a/src/app/dashboard/(auth)/roles/page.tsx +++ b/src/app/dashboard/(auth)/roles/page.tsx @@ -9,6 +9,8 @@ import Link from "next/link"; import { CreateModal } from "./_modals"; import FormModal from "./_modals/FormModal"; import CreateButton from "./_components/CreateButton/CreateButton"; +import getRoles from "@/features/dashboard/roles/data/getRoles"; +import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions"; interface Props { searchParams: { @@ -20,20 +22,25 @@ interface Props { } export default async function RolesPage({searchParams}: Props) { - if (!(await checkPermission("role.readAll"))) { - return unauthorized(); - } - const allowCreate = await checkPermission("role.create") + const permissions = await checkMultiplePermissions({ + create: "role.create", + readAll: "role.readAll", + read: 'role.read', + update: 'role.update', + delete: 'role.delete' + }) + + const roles = await getRoles() return ( Roles - { allowCreate && } + { permissions.create && } - + ); diff --git a/src/features/auth/tools/checkMultiplePermissions.ts b/src/features/auth/tools/checkMultiplePermissions.ts new file mode 100644 index 0000000..afa6fba --- /dev/null +++ b/src/features/auth/tools/checkMultiplePermissions.ts @@ -0,0 +1,31 @@ +import { getUserFromToken } from "../authUtils"; +import getCurrentUser from "./getCurrentUser"; + +type PermissionsInput = Record; + +type PermissionsOutput = Record; + +/** + * Checks multiple permissions for the current user and returns an object indicating + * whether each permission is granted. + * + * @param {PermissionsInput} permissions - An object with keys as permission names and values as the required roles/permissions. + * @returns {Promise} An object with keys as permission names and boolean values indicating whether the permission is granted. + */ +async function checkMultiplePermissions>(permissions: T): Promise<{ [K in keyof T]: boolean }> { + const permissionResults: Partial<{ [K in keyof T]: boolean }> = {}; + const currentUser = await getCurrentUser(); + + for (const permissionKey in permissions) { + if (permissions.hasOwnProperty(permissionKey)) { + const requiredPermission = permissions[permissionKey]; + // const isPermissionGranted: boolean = currentUser ? currentUser.roles.includes(requiredPermission) : false; + const isPermissionGranted = true; + permissionResults[permissionKey] = isPermissionGranted; + } + } + + return permissionResults as { [K in keyof T]: boolean }; +} + +export default checkMultiplePermissions; diff --git a/src/features/dashboard/roles/actions/upsertRole.ts b/src/features/dashboard/roles/actions/upsertRole.ts index ce94a64..131f031 100644 --- a/src/features/dashboard/roles/actions/upsertRole.ts +++ b/src/features/dashboard/roles/actions/upsertRole.ts @@ -1,4 +1,5 @@ -"use server" +"use server"; + import checkPermission from "@/features/auth/tools/checkPermission"; import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData"; import { unauthorized } from "@/BaseError"; @@ -6,63 +7,64 @@ import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; import prisma from "@/db"; import { revalidatePath } from "next/cache"; -export default async function upsertRole(data: RoleFormData){ - - const isInsert = !!data.id; +/** + * Upserts a role based on the provided RoleFormData. + * If the role already exists (determined by `id`), it updates the role; otherwise, it creates a new role. + * Authorization checks are performed based on whether it's a create or update operation. + * + * @param {RoleFormData} data - The data for creating or updating the role. + * @returns {Promise} An object containing the success status, message, and any errors. + */ +export default async function upsertRole(data: RoleFormData) { + const isInsert = !data.id; - if (isInsert && !await checkPermission("role.create")){ - return unauthorized(); - } - - if (!isInsert && !await checkPermission("role.update")){ + // Authorization check + const permissionType = isInsert ? "role.create" : "role.update"; + if (!await checkPermission(permissionType)) { return unauthorized(); } + // Validate form data const validatedFields = roleFormDataSchema.safeParse(data); - if (!validatedFields.success){ + if (!validatedFields.success) { return { success: false, message: "Invalid Form Data", - errors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors) - } as const + errors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors), + }; } - // Update user data in the database try { - if (isInsert){ - await prisma.role.update({ - where: { id: validatedFields.data.id!}, - data: { - code: validatedFields.data.code, - description: validatedFields.data.description, - isActive: validatedFields.data.isActive, - name: validatedFields.data.name - }, - }) + const roleData = { + code: validatedFields.data.code, + description: validatedFields.data.description, + name: validatedFields.data.name, + isActive: validatedFields.data.isActive, + }; + + // Database operation + if (isInsert) { + await prisma.role.create({ data: roleData }); } else { - await prisma.role.create({ - data: { - code: validatedFields.data.code, - description: validatedFields.data.description, - name: validatedFields.data.name, - isActive: validatedFields.data.isActive - } - }) + await prisma.role.update({ + where: { id: validatedFields.data.id! }, + data: roleData, + }); } // Revalidate the cache revalidatePath("."); + // Return success message return { success: true, - message: `Role ${validatedFields.data.name} has been successfully ${isInsert ? "Updated" : "Created"}` + message: `Role ${validatedFields.data.name} has been successfully ${isInsert ? "created" : "updated"}.`, }; } catch (error) { - // Consider handling specific database errors here - console.error('Error updating user data', error); + console.error('Error updating role data', error); return { success: false, - message: "Error updating user data" + message: "Error updating role data.", }; } -} \ No newline at end of file +} diff --git a/src/features/dashboard/roles/data/getRoles.ts b/src/features/dashboard/roles/data/getRoles.ts new file mode 100644 index 0000000..e34cc9b --- /dev/null +++ b/src/features/dashboard/roles/data/getRoles.ts @@ -0,0 +1,45 @@ +import { unauthorized } from "@/BaseError"; +import prisma from "@/db"; +import checkPermission from "@/features/auth/tools/checkPermission"; +import "server-only"; + +/** + * Retrieves all roles along with the count of associated permissions and users. + * Authorization check is performed for the operation. + * + * @returns An array of role objects each including details and counts of related permissions and users. + */ +export default async function getRoles() { + // Authorization check + if (!await checkPermission("roles.getAll")) { + return unauthorized(); + } + + try { + // Fetch roles from the database + const roles = await prisma.role.findMany({ + include: { + _count: { + select: { + permissions: true, + users: true + } + } + } + }); + + // Transform the data into the desired format + return roles.map(({ id, code, name, description, isActive, _count }) => ({ + id, + code, + name, + description, + isActive, + permissionCount: _count.permissions, + userCount: _count.users + })); + } catch (error) { + console.error('Error retrieving roles', error); + throw error; + } +} diff --git a/src/features/dashboard/utils/createActionButtons.tsx b/src/features/dashboard/utils/createActionButtons.tsx new file mode 100644 index 0000000..59875b9 --- /dev/null +++ b/src/features/dashboard/utils/createActionButtons.tsx @@ -0,0 +1,50 @@ +import { + ActionIcon, + ActionIconVariant, + MantineColor, + Tooltip, +} from "@mantine/core"; +import Link from "next/link"; +import React from "react"; + +interface Action { + label: string; + action?: () => void | string; + variant?: ActionIconVariant; + permission?: boolean; + icon: React.ReactNode; + color: MantineColor; +} + +export default function createActionButtons(actions: Action[]) { + const defaults = { + variant: "light", + }; + + const elements = actions.map((action, i) => + action.permission ? ( + + {typeof action.action === "string" || action.action === undefined ? ( + + {action.icon} + + ) : ( + + {action.icon} + + )} + + ) : null + ); + + return <>{elements}; +}