From 9ba63c2ec95d9fabf10105ad89320cdda37e9f50 Mon Sep 17 00:00:00 2001 From: sianida26 Date: Wed, 14 Feb 2024 14:24:29 +0700 Subject: [PATCH] Optimize permissions --- .../permissions/actions/upsertPermission.ts | 2 +- .../dashboard/users/actions/deleteUser.ts | 44 ---- .../dashboard/users/actions/editUser.ts | 69 ------ .../dashboard/users/data/getUserDetailById.ts | 45 ---- .../dashboard/users/data/getUsers.ts | 32 --- .../users/formSchemas/userFormDataSchema.ts | 17 -- src/app/dashboard/(auth)/permissions/page.tsx | 15 +- .../permission/actions/deletePermission.ts | 27 +++ .../permission/actions/getAllPermissions.ts | 56 +++++ .../permission/actions/getPermissionById.ts | 49 ++++ .../permission/actions/upsertPermission.ts | 83 +++++++ .../formSchemas/PermissionFormData.ts | 0 .../modals/PermissionDeleteModal.tsx | 105 +++++++++ .../permission/modals/PermissionFormModal.tsx | 213 ++++++++++++++++++ .../PermissionTable/PermissionTable.tsx | 126 +++++++++++ .../tables/PermissionTable/columns.tsx | 105 +++++++++ src/modules/permission/types/Permission.d.ts | 11 + 17 files changed, 783 insertions(+), 216 deletions(-) delete mode 100644 src/_features/dashboard/users/actions/deleteUser.ts delete mode 100644 src/_features/dashboard/users/actions/editUser.ts delete mode 100644 src/_features/dashboard/users/data/getUserDetailById.ts delete mode 100644 src/_features/dashboard/users/data/getUsers.ts delete mode 100644 src/_features/dashboard/users/formSchemas/userFormDataSchema.ts create mode 100644 src/modules/permission/actions/deletePermission.ts create mode 100644 src/modules/permission/actions/getAllPermissions.ts create mode 100644 src/modules/permission/actions/getPermissionById.ts create mode 100644 src/modules/permission/actions/upsertPermission.ts rename src/{_features/dashboard/permissions => modules/permission}/formSchemas/PermissionFormData.ts (100%) create mode 100644 src/modules/permission/modals/PermissionDeleteModal.tsx create mode 100644 src/modules/permission/modals/PermissionFormModal.tsx create mode 100644 src/modules/permission/tables/PermissionTable/PermissionTable.tsx create mode 100644 src/modules/permission/tables/PermissionTable/columns.tsx create mode 100644 src/modules/permission/types/Permission.d.ts diff --git a/src/_features/dashboard/permissions/actions/upsertPermission.ts b/src/_features/dashboard/permissions/actions/upsertPermission.ts index 4a92f7e..e9cb39f 100644 --- a/src/_features/dashboard/permissions/actions/upsertPermission.ts +++ b/src/_features/dashboard/permissions/actions/upsertPermission.ts @@ -1,7 +1,7 @@ "use server"; import checkPermission from "@/features/auth/tools/checkPermission"; -import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData"; +import permissionFormDataSchema, { PermissionFormData } from "../../../../modules/permission/formSchemas/PermissionFormData"; import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; import prisma from "@/db"; import { revalidatePath } from "next/cache"; diff --git a/src/_features/dashboard/users/actions/deleteUser.ts b/src/_features/dashboard/users/actions/deleteUser.ts deleted file mode 100644 index 9ad686b..0000000 --- a/src/_features/dashboard/users/actions/deleteUser.ts +++ /dev/null @@ -1,44 +0,0 @@ -"use server"; - -import checkPermission from "@/features/auth/tools/checkPermission"; -import prisma from "@/db"; -import { revalidatePath } from "next/cache"; - -/** - * Deletes a user from the database based on their ID. - * - * @param {string} id The unique identifier of the user to be deleted. - * @returns A promise that resolves to an object indicating the success or failure of the operation. - */ -export default async function deleteUser(id: string) { - - // Check user permission - if (!(await checkPermission())) return { - success: false, - message: "Unauthorized" - } as const; - - // Find the user in the database - const user = await prisma.user.findFirst({ - where: { id }, - }); - - // Handle case where user is not found - if (!user) return { - success: false, - message: "User not found" - } as const; - - // Delete the user - await prisma.user.delete({ - where: { id } - }); - - // Revalidate cache - revalidatePath("."); - - return { - success: true, - message: `User ${user.name} has been successfully deleted` - } as const; -} diff --git a/src/_features/dashboard/users/actions/editUser.ts b/src/_features/dashboard/users/actions/editUser.ts deleted file mode 100644 index c3de03f..0000000 --- a/src/_features/dashboard/users/actions/editUser.ts +++ /dev/null @@ -1,69 +0,0 @@ -"use server"; - -import prisma from "@/db"; -import userFormDataSchema, { UserFormData } from "../formSchemas/userFormDataSchema"; -import checkPermission from "@/features/auth/tools/checkPermission"; -import { revalidatePath } from "next/cache"; -import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; - -/** - * Edits user data in the database based on the provided form data. - * - * @param formData The user data to be updated. - * @returns A promise that resolves to an object indicating the success or failure of the operation. - */ -export default async function editUser(formData: UserFormData) { - - // Check user permission - if (!await checkPermission("authenticated-only")) return { - success: false, - message: "Unauthorized" - } - - // Validate form data - const validatedFields = userFormDataSchema.safeParse(formData); - if (!validatedFields.success){ - return { - success: false, - message: "Invalid Form Data", - errors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors) - } as const - } - - // Check for valid ID - if (!validatedFields.data.id){ - return { - success: false, - message: "Invalid Form Data", - errors: { - id: "Invalid ID" - } - } as const - } - - // Update user data in the database - try { - await prisma.user.update({ - where: { id: validatedFields.data.id }, - data: { - email: validatedFields.data.email, - name: validatedFields.data.name, - } - }); - - // Revalidate the cache - revalidatePath("."); - - return { - success: true, - message: `User ${validatedFields.data.name} has been successfully updated` - }; - } catch (error) { - // Consider handling specific database errors here - console.error('Error updating user data', error); - return { - success: false, - message: "Error updating user data" - }; - } -} diff --git a/src/_features/dashboard/users/data/getUserDetailById.ts b/src/_features/dashboard/users/data/getUserDetailById.ts deleted file mode 100644 index b61427e..0000000 --- a/src/_features/dashboard/users/data/getUserDetailById.ts +++ /dev/null @@ -1,45 +0,0 @@ -import "server-only" -import prisma from "@/db"; -import { notFound } from "next/navigation"; -import checkPermission from "@/features/auth/tools/checkPermission"; -import { unauthorized } from "@/BaseError"; - -/** - * Retrieves detailed information of a user by their ID. - * - * @param id The unique identifier of the user. - * @returns The user's detailed information or an error response. - */ -export default async function getUserDetailById(id: string){ - - // Check user permission - if (!checkPermission("authenticated-only")) return unauthorized(); - - // Retrieve user data from the database - const user = await prisma.user.findFirst({ - where: { id }, - select: { - id: true, - email: true, - name: true, - photoProfile: { - select: { - path: true - } - }, - } - }) - - // Check if user exists - if (!user) return notFound(); - - // Format user data - const formattedUser = { - id: user.id, - email: user.email ?? "", - name: user.name ?? "", - photoProfileUrl: user.photoProfile?.path ?? "" - } - - return formattedUser; -} diff --git a/src/_features/dashboard/users/data/getUsers.ts b/src/_features/dashboard/users/data/getUsers.ts deleted file mode 100644 index f086209..0000000 --- a/src/_features/dashboard/users/data/getUsers.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { unauthorized } from "@/BaseError" -import prisma from "@/db"; -import checkPermission from "@/features/auth/tools/checkPermission" -import "server-only" - -const getUsers = async () => { - - if (!await checkPermission("authenticated-only")) unauthorized(); - - const users = await prisma.user.findMany({ - select: { - id: true, - email: true, - photoProfile: { - select: { - path: true - } - }, - name: true, - }, - }) - - const result = users.map((user) => ({ - ...user, - photoUrl: user.photoProfile?.path ?? null, - photoProfile: undefined - })) - - return result; -} - -export default getUsers; diff --git a/src/_features/dashboard/users/formSchemas/userFormDataSchema.ts b/src/_features/dashboard/users/formSchemas/userFormDataSchema.ts deleted file mode 100644 index 24b1f0e..0000000 --- a/src/_features/dashboard/users/formSchemas/userFormDataSchema.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { z } from "zod"; - -export interface UserFormData { - id: string; - name: string; - photoProfileUrl: string; - email: string; -} - -const userFormDataSchema = z.object({ - id: z.string().nullable(), - name: z.string(), - photoProfileUrl: z.union([z.string(), z.null()]), - email: z.string().email(), -}); - -export default userFormDataSchema; diff --git a/src/app/dashboard/(auth)/permissions/page.tsx b/src/app/dashboard/(auth)/permissions/page.tsx index 3b76d8a..88fd333 100644 --- a/src/app/dashboard/(auth)/permissions/page.tsx +++ b/src/app/dashboard/(auth)/permissions/page.tsx @@ -1,11 +1,9 @@ +import checkMultiplePermissions from "@/modules/dashboard/services/checkMultiplePermissions"; +import getAllPermissions from "@/modules/permission/actions/getAllPermissions"; +import PermissionsTable from "@/modules/permission/tables/PermissionTable/PermissionTable"; import { Card, Stack, Title } from "@mantine/core"; import { Metadata } from "next"; import React from "react"; -// import RolesTable from "./_tables/RolesTable/RolesTable"; -// import getRoles from "@/features/dashboard/roles/data/getRoles"; -import checkMultiplePermissions from "@/features/auth/tools/checkMultiplePermissions"; -import { PermissionTable } from "@/features/dashboard/permissions/tables"; -import getPermissions from "@/features/dashboard/permissions/data/getPermissions"; interface Props { searchParams: { @@ -28,14 +26,15 @@ export default async function RolesPage({ searchParams }: Props) { update: "permission.update", delete: "permission.delete", }); - - const permissionData = await getPermissions() + + const res = await getAllPermissions(); + if (!res.success) throw new Error(); return ( Permissions - + ); diff --git a/src/modules/permission/actions/deletePermission.ts b/src/modules/permission/actions/deletePermission.ts new file mode 100644 index 0000000..4e4e187 --- /dev/null +++ b/src/modules/permission/actions/deletePermission.ts @@ -0,0 +1,27 @@ +"use server"; + +import prisma from "@/db"; +import checkPermission from "@/modules/dashboard/services/checkPermission"; +import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; +import handleCatch from "@/modules/dashboard/utils/handleCatch"; +import unauthorized from "@/modules/dashboard/utils/unauthorized"; +import { revalidatePath } from "next/cache"; + +export default async function deletePermission(id: string): Promise { + try { + if (!(await checkPermission("permission.delete"))) unauthorized(); + const permission = await prisma.permission.delete({ + where: { id }, + }); + + + revalidatePath(".") + + return { + success: true, + message: "The permission has been deleted successfully", + }; + } catch (e: unknown) { + return handleCatch(e) + } +} diff --git a/src/modules/permission/actions/getAllPermissions.ts b/src/modules/permission/actions/getAllPermissions.ts new file mode 100644 index 0000000..419c60a --- /dev/null +++ b/src/modules/permission/actions/getAllPermissions.ts @@ -0,0 +1,56 @@ +"use server" +import prisma from "@/db"; +import checkPermission from "@/modules/dashboard/services/checkPermission"; +import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; +import handleCatch from "@/modules/dashboard/utils/handleCatch"; +import unauthorized from "@/modules/dashboard/utils/unauthorized"; +import "server-only"; +import Permission from "../types/Permission"; + +/** + * Retrieves all permissions along with the count of associated permissions and users. + * Authorization check is performed for the operation. + * + * @returns An array of permission objects each including details and counts of related permissions and users. + */ +export default async function getAllPermissions(): Promise> { + // Authorization check + if (!(await checkPermission("permissions.readAll"))) { + unauthorized(); + } + + try { + // Fetch permissions from the database + const permissions = await prisma.permission.findMany({ + include: { + _count: { + select: { + roles: true, + directUsers: true, + }, + }, + }, + }); + + // Transform the data into the desired format + const permissionsData = permissions.map( + ({ id, code, name, description, isActive, _count }) => ({ + id, + code, + name, + description, + isActive, + roleCount: _count.roles, + //User count counts only direct user + userCount: _count.directUsers, + }) + ); + + return { + success: true, + data: permissionsData + } + } catch (error) { + return handleCatch(error) + } +} diff --git a/src/modules/permission/actions/getPermissionById.ts b/src/modules/permission/actions/getPermissionById.ts new file mode 100644 index 0000000..1ed5f81 --- /dev/null +++ b/src/modules/permission/actions/getPermissionById.ts @@ -0,0 +1,49 @@ +"use server"; + +import prisma from "@/db"; +import checkPermission from "@/modules/dashboard/services/checkPermission"; +import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; +import handleCatch from "@/modules/dashboard/utils/handleCatch"; +import unauthorized from "@/modules/dashboard/utils/unauthorized"; + +interface Permission { + id: string; + code: string; + name: string; + description: string; + isActive: boolean; +} + +export default async function getPermissionById( + id: string +): Promise> { + try { + if (!(await checkPermission("permissions.read"))) unauthorized(); + + const permission = await prisma.permission.findFirst({ + where: { id }, + select: { + code: true, + description: true, + id: true, + isActive: true, + name: true, + }, + }); + + if (!permission) { + return { + success: false, + message: "Permission not found", + } as const; + } + + return { + success: true, + message: "Permission fetched successfully", + data: permission, + } as const; + } catch (e) { + return handleCatch(e); + } +} diff --git a/src/modules/permission/actions/upsertPermission.ts b/src/modules/permission/actions/upsertPermission.ts new file mode 100644 index 0000000..51a6258 --- /dev/null +++ b/src/modules/permission/actions/upsertPermission.ts @@ -0,0 +1,83 @@ +"use server"; + +import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData"; +import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; +import prisma from "@/db"; +import { revalidatePath } from "next/cache"; +import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; +import checkPermission from "@/modules/dashboard/services/checkPermission"; +import unauthorized from "@/modules/dashboard/utils/unauthorized"; +import DashboardError from "@/modules/dashboard/errors/DashboardError"; +import handleCatch from "@/modules/dashboard/utils/handleCatch"; + +/** + * Upserts a permission based on the provided PermissionFormData. + * If the permission already exists (determined by `id`), it updates the permission; otherwise, it creates a new permission. + * Authorization checks are performed based on whether it's a create or update operation. + * + * @param data - The data for creating or updating the permission. + * @returns An object containing the success status, message, and any errors. + */ +export default async function upsertPermission( + data: PermissionFormData +): Promise { + try { + const isInsert = !data.id; + + // Authorization check + const permissionType = isInsert ? "permission.create" : "permission.update"; + if (!(await checkPermission(permissionType))) { + unauthorized(); + } + + // Validate form data + const validatedFields = permissionFormDataSchema.safeParse(data); + if (!validatedFields.success) { + throw new DashboardError({ + errorCode: "INVALID_FORM_DATA", + formErrors: mapObjectToFirstValue(validatedFields.error.flatten().fieldErrors) + }) + } + const permissionData = { + code: validatedFields.data.code, + description: validatedFields.data.description, + name: validatedFields.data.name, + isActive: validatedFields.data.isActive, + }; + + // Database operation + if (isInsert) { + if (await prisma.permission.findFirst({ + where: { + code: permissionData.code + } + })){ + throw new DashboardError({ + errorCode: "INVALID_FORM_DATA", + formErrors: { + code: "The code is already exists" + } + }) + } + await prisma.permission.create({ data: permissionData }); + } else { + await prisma.permission.update({ + where: { id: validatedFields.data.id! }, + data: permissionData, + }); + } + + // Revalidate the cache + revalidatePath("."); + + // Return success message + return { + success: true, + message: `Permission ${validatedFields.data.name} has been successfully ${ + isInsert ? "created" : "updated" + }.`, + }; + } catch (error) { + return handleCatch(error) + } +} diff --git a/src/_features/dashboard/permissions/formSchemas/PermissionFormData.ts b/src/modules/permission/formSchemas/PermissionFormData.ts similarity index 100% rename from src/_features/dashboard/permissions/formSchemas/PermissionFormData.ts rename to src/modules/permission/formSchemas/PermissionFormData.ts diff --git a/src/modules/permission/modals/PermissionDeleteModal.tsx b/src/modules/permission/modals/PermissionDeleteModal.tsx new file mode 100644 index 0000000..910a402 --- /dev/null +++ b/src/modules/permission/modals/PermissionDeleteModal.tsx @@ -0,0 +1,105 @@ +"use client"; +import { useRouter } from "next/navigation"; +import React, { useState } from "react"; +import { + Button, + Flex, + Modal, + Text, + Alert, +} from "@mantine/core"; +import { showNotification } from "@/utils/notifications"; +import { error } from "console"; +import { revalidatePath } from "next/cache"; +import withServerAction from "@/modules/dashboard/utils/withServerAction"; +import deletePermission from "../actions/deletePermission"; +import DashboardError from "@/modules/dashboard/errors/DashboardError"; + +export interface DeleteModalProps { + data?: { + id: string; + name: string; + }; + onClose: () => void; +} + +export default function DeleteModal(props: DeleteModalProps) { + const router = useRouter(); + + const [isSubmitting, setSubmitting] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + /** + * Closes the modal. It won't close if a submission is in progress. + */ + const closeModal = () => { + if (isSubmitting) return; + setErrorMessage("") + props.onClose(); + }; + + const confirmAction = () => { + if (!props.data?.id) return; + setSubmitting(true); + + withServerAction(deletePermission, props.data!.id) + .then((response) => { + showNotification( + response.message ?? "Permission deleted successfully" + ); + setSubmitting(false); + props.onClose() + }) + .catch((e) => { + if (e instanceof DashboardError){ + setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`) + } + else if (e instanceof Error) { + setErrorMessage(`ERROR: ${e.message}`) + } else { + setErrorMessage(`Unkown error is occured. Please contact administrator`) + } + }) + .finally(() => { + setSubmitting(false) + }); + }; + + return ( + + + Are you sure you want to delete permission{" "} + + {props.data?.name} + + ? This action is irreversible. + + + {errorMessage && {errorMessage}} + {/* Buttons */} + + + + + + ); +} \ No newline at end of file diff --git a/src/modules/permission/modals/PermissionFormModal.tsx b/src/modules/permission/modals/PermissionFormModal.tsx new file mode 100644 index 0000000..3428bd6 --- /dev/null +++ b/src/modules/permission/modals/PermissionFormModal.tsx @@ -0,0 +1,213 @@ +/* eslint-disable react-hooks/exhaustive-deps */ +import { showNotification } from "@/utils/notifications"; +import { + Flex, + Modal, + Stack, + Switch, + TextInput, + Textarea, + Button, + ScrollArea, + Checkbox, + Skeleton, + Fieldset, + Alert, +} from "@mantine/core"; +import { useForm, zodResolver } from "@mantine/form"; +import { useRouter } from "next/navigation"; +import React, { useCallback, useEffect, useState } from "react"; +import { TbDeviceFloppy } from "react-icons/tb"; +import permissionFormDataSchema, { PermissionFormData } from "../formSchemas/PermissionFormData"; +import getPermissionById from "../actions/getPermissionById"; +import withServerAction from "@/modules/dashboard/utils/withServerAction"; +import upsertPermission from "../actions/upsertPermission"; +import DashboardError from "@/modules/dashboard/errors/DashboardError"; + +export interface ModalProps { + title: string; + readonly?: boolean; + id?: string; + opened: boolean; + onClose?: () => void; +} + +/** + * A component for rendering a modal with a form to create or edit a permission. + * + * @param props - The props for the component. + * @returns The rendered element. + */ +export default function FormModal(props: ModalProps) { + const router = useRouter(); + const [isSubmitting, setSubmitting] = useState(false); + const [isFetching, setFetching] = useState(false); + const [errorMessage, setErrorMessage] = useState(""); + + const form = useForm({ + initialValues: { + code: "", + description: "", + id: "", + isActive: false, + name: "", + }, + validate: zodResolver(permissionFormDataSchema), + validateInputOnChange: false, + onValuesChange: (values) => { + console.log(values); + }, + }); + + /** + * Fetches permission data by ID and populates the form if the modal is opened and an ID is provided. + */ + useEffect(() => { + if (!props.opened || !props.id) { + return; + } + + setFetching(true); + getPermissionById(props.id) + .then((response) => { + if (response.success) { + const data = response.data; + form.setValues({ + code: data.code, + description: data.description, + id: data.id, + isActive: data.isActive, + name: data.name, + }); + } + }) + .catch((e) => { + //TODO: Handle error + console.log(e); + }) + .finally(() => { + setFetching(false); + }); + }, [props.opened, props.id]); + + const closeModal = () => { + form.reset() + props.onClose ? props.onClose() : router.replace("?"); + }; + + const handleSubmit = (values: PermissionFormData) => { + setSubmitting(true); + withServerAction(upsertPermission, values) + .then((response) => { + showNotification(response.message!, "success"); + closeModal(); + }) + .catch((e) => { + if (e instanceof DashboardError) { + if (e.errorCode === "INVALID_FORM_DATA") { + form.setErrors(e.formErrors ?? {}); + } else { + setErrorMessage(`ERROR: ${e.message} (${e.errorCode})`); + } + } else if (e instanceof Error) { + setErrorMessage(`ERROR: ${e.message}`); + } else { + setErrorMessage( + `Unkown error is occured. Please contact administrator` + ); + } + }) + .finally(() => { + setSubmitting(false); + }); + }; + + return ( + +
+ + {errorMessage && {errorMessage}} + {/* ID */} + {form.values.id ? ( + + ) : ( +
+ )} + + {/* Code */} + + + + + {/* Name */} + + + + + {/* Description */} + +