From e33a7b5e9ce71255d2000e397671695a27514579 Mon Sep 17 00:00:00 2001 From: sianida26 Date: Fri, 29 Mar 2024 13:38:17 +0700 Subject: [PATCH] Change into action-service pattern --- src/app/dashboard/users/page.tsx | 4 +- .../dashboard/errors/DashboardError.ts | 2 + src/modules/dashboard/utils/notFound.ts | 5 +- .../userManagement/actions/deleteUser.ts | 46 ------- .../actions/deleteUserAction.ts | 29 +++++ .../userManagement/actions/getAllUsers.ts | 38 ------ .../actions/getAllUsersAction.ts | 24 ++++ ...tailById.ts => getUserDetailByIdAction.ts} | 32 +---- .../userManagement/actions/upsertUser.ts | 117 ------------------ .../actions/upsertUserAction.ts | 46 +++++++ .../errors/UserManagementError.ts | 2 + .../userManagement/modals/UserDeleteModal.tsx | 4 +- .../userManagement/modals/UserFormModal.tsx | 6 +- .../userManagement/services/deleteUser.ts | 26 ++++ .../userManagement/services/getAllUsers.ts | 29 +++++ .../userManagement/services/getUserById.ts | 24 ++++ .../userManagement/services/upsertUser.ts | 80 ++++++++++++ .../tables/UsersTable/UsersTable.tsx | 8 +- 18 files changed, 280 insertions(+), 242 deletions(-) delete mode 100644 src/modules/userManagement/actions/deleteUser.ts create mode 100644 src/modules/userManagement/actions/deleteUserAction.ts delete mode 100644 src/modules/userManagement/actions/getAllUsers.ts create mode 100644 src/modules/userManagement/actions/getAllUsersAction.ts rename src/modules/userManagement/actions/{getUserDetailById.ts => getUserDetailByIdAction.ts} (61%) delete mode 100644 src/modules/userManagement/actions/upsertUser.ts create mode 100644 src/modules/userManagement/actions/upsertUserAction.ts create mode 100644 src/modules/userManagement/services/deleteUser.ts create mode 100644 src/modules/userManagement/services/getAllUsers.ts create mode 100644 src/modules/userManagement/services/getUserById.ts create mode 100644 src/modules/userManagement/services/upsertUser.ts diff --git a/src/app/dashboard/users/page.tsx b/src/app/dashboard/users/page.tsx index 8b9c747..ed2f9cd 100644 --- a/src/app/dashboard/users/page.tsx +++ b/src/app/dashboard/users/page.tsx @@ -1,6 +1,6 @@ import { Card, Stack, Title } from "@mantine/core"; import React from "react"; -import getUsers from "@/modules/userManagement/actions/getAllUsers"; +import getUsers from "@/modules/userManagement/services/getAllUsers"; import { Metadata } from "next"; import UsersTable from "@/modules/userManagement/tables/UsersTable/UsersTable"; import checkMultiplePermissions from "@/modules/auth/utils/checkMultiplePermissions"; @@ -25,7 +25,7 @@ export default async function UsersPage() { Users - + ); diff --git a/src/modules/dashboard/errors/DashboardError.ts b/src/modules/dashboard/errors/DashboardError.ts index af3f052..c269fdc 100644 --- a/src/modules/dashboard/errors/DashboardError.ts +++ b/src/modules/dashboard/errors/DashboardError.ts @@ -14,6 +14,7 @@ interface DashboardErrorOptions { message?: string; errorCode: (typeof DashboardErrorCodes)[number] | (string & {}); formErrors?: Record + statusCode?: number; } export default class DashboardError extends BaseError { @@ -24,6 +25,7 @@ export default class DashboardError extends BaseError { super({ errorCode: options.errorCode, message: options.message, + statusCode: options.statusCode, }); this.errorCode = options.errorCode; diff --git a/src/modules/dashboard/utils/notFound.ts b/src/modules/dashboard/utils/notFound.ts index 3fa9b71..1819010 100644 --- a/src/modules/dashboard/utils/notFound.ts +++ b/src/modules/dashboard/utils/notFound.ts @@ -1,15 +1,16 @@ -import DashboardError from "../errors/DashboardError"; +import BaseError from "@/core/error/BaseError"; /** * Throws a 'NOT_FOUND' DashboardError with a custom or default message. * @param message Optional custom message for the error. */ const notFound = ({ message }: { message?: string }) => { - throw new DashboardError({ + throw new BaseError({ errorCode: "NOT_FOUND", message: message ?? "The requested data could not be located. It may have been deleted or relocated. Please verify the information or try a different request.", + statusCode: 404 }); }; diff --git a/src/modules/userManagement/actions/deleteUser.ts b/src/modules/userManagement/actions/deleteUser.ts deleted file mode 100644 index 81320ec..0000000 --- a/src/modules/userManagement/actions/deleteUser.ts +++ /dev/null @@ -1,46 +0,0 @@ -"use server"; - -import prisma from "@/db"; -import getCurrentUser from "@/modules/auth/utils/getCurrentUser"; -import checkPermission from "@/modules/dashboard/services/checkPermission"; -import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; -import handleCatch from "@/modules/dashboard/utils/handleCatch"; -import notFound from "@/modules/dashboard/utils/notFound"; -import unauthorized from "@/modules/dashboard/utils/unauthorized"; -import { revalidatePath } from "next/cache"; -import UserManagementError from "../errors/UserManagementError"; -import db from "@/core/db"; - -export default async function deleteUser( - id: string -): Promise { - try { - const currentUser = await getCurrentUser(); - - if (!(await checkPermission("users.delete")) || !currentUser) - return unauthorized(); - - //prevents self delete - if (currentUser.id === id) { - throw new UserManagementError({ - errorCode: "CANNOT_DELETE_SELF", - message: "You cannot delete yourself", - }); - } - - const user = await db.user.delete({ - where: { id }, - }); - - if (!user) notFound({ message: "The user does not exists" }); - - revalidatePath("."); - - return { - success: true, - message: "The user has been deleted successfully", - }; - } catch (e: unknown) { - return handleCatch(e); - } -} diff --git a/src/modules/userManagement/actions/deleteUserAction.ts b/src/modules/userManagement/actions/deleteUserAction.ts new file mode 100644 index 0000000..ae512f4 --- /dev/null +++ b/src/modules/userManagement/actions/deleteUserAction.ts @@ -0,0 +1,29 @@ +"use server"; + +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"; +import deleteUser from "../services/deleteUser"; +import checkPermission from "@/modules/auth/utils/checkPermission"; + +export default async function deleteUserAction( + id: string +): Promise { + try { + + if (!(await checkPermission("users.delete"))) + return unauthorized(); + + await deleteUser(id); + + revalidatePath("."); + + return { + success: true, + message: "The user has been deleted successfully", + }; + } catch (e: unknown) { + return handleCatch(e); + } +} diff --git a/src/modules/userManagement/actions/getAllUsers.ts b/src/modules/userManagement/actions/getAllUsers.ts deleted file mode 100644 index a62f592..0000000 --- a/src/modules/userManagement/actions/getAllUsers.ts +++ /dev/null @@ -1,38 +0,0 @@ -import db from "@/core/db"; -import prisma from "@/db"; -import checkPermission from "@/modules/dashboard/services/checkPermission"; -import unauthorized from "@/modules/dashboard/utils/unauthorized"; -import "server-only"; - -const getAllUsers = async () => { - if (!(await checkPermission("users.readAll"))) unauthorized(); - - try { - const users = await db.user.findMany({ - select: { - id: true, - email: true, - photoProfile: true, - name: true, - roles: { - select: { - name: true, - code: true, - }, - }, - }, - }); - - const result = users.map((user) => ({ - ...user, - photoUrl: user.photoProfile ?? null, - photoProfile: undefined, - })); - - return result; - } catch (e) { - throw e; - } -}; - -export default getAllUsers; diff --git a/src/modules/userManagement/actions/getAllUsersAction.ts b/src/modules/userManagement/actions/getAllUsersAction.ts new file mode 100644 index 0000000..e1c78c6 --- /dev/null +++ b/src/modules/userManagement/actions/getAllUsersAction.ts @@ -0,0 +1,24 @@ +"use server"; + +import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; +import getAllUsers from "../services/getAllUsers"; +import handleCatch from "@/modules/dashboard/utils/handleCatch"; +import checkPermission from "@/modules/auth/utils/checkPermission"; +import unauthorized from "@/modules/dashboard/utils/unauthorized"; + +export default async function getAllUsersAction(): Promise< + ServerResponseAction>> +> { + try { + if (!(await checkPermission("users.readAll"))) unauthorized(); + + const users = await getAllUsers(); + + return { + success: true, + data: users, + }; + } catch (e) { + return handleCatch(e); + } +} diff --git a/src/modules/userManagement/actions/getUserDetailById.ts b/src/modules/userManagement/actions/getUserDetailByIdAction.ts similarity index 61% rename from src/modules/userManagement/actions/getUserDetailById.ts rename to src/modules/userManagement/actions/getUserDetailByIdAction.ts index ff55d52..dc72e5b 100644 --- a/src/modules/userManagement/actions/getUserDetailById.ts +++ b/src/modules/userManagement/actions/getUserDetailByIdAction.ts @@ -1,10 +1,8 @@ "use server"; -import "server-only"; -import prisma from "@/db"; -import checkPermission from "@/modules/dashboard/services/checkPermission"; import unauthorized from "@/modules/dashboard/utils/unauthorized"; import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; -import db from "@/core/db"; +import getUserById from "../services/getUserById"; +import checkPermission from "@/modules/auth/utils/checkPermission"; type UserData = { id: string; @@ -23,35 +21,14 @@ type UserData = { * @param id The unique identifier of the user. * @returns The user's detailed information or an error response. */ -export default async function getUserDetailById( +export default async function getUserDetailByIdAction( id: string ): Promise> { // Check user permission if (!checkPermission("users.read")) return unauthorized(); // Retrieve user data from the database - const user = await db.user.findFirst({ - where: { id }, - select: { - id: true, - email: true, - name: true, - photoProfile: true, - roles: { - select: { - code: true, - name: true, - }, - }, - }, - }); - - // Check if user exists - if (!user) - return { - success: false, - message: "User not found", - } as const; + const user = await getUserById(id) // Format user data const formattedUser = { @@ -64,7 +41,6 @@ export default async function getUserDetailById( return { success: true, - message: "Permission fetched successfully", data: formattedUser, } as const; } diff --git a/src/modules/userManagement/actions/upsertUser.ts b/src/modules/userManagement/actions/upsertUser.ts deleted file mode 100644 index be0977c..0000000 --- a/src/modules/userManagement/actions/upsertUser.ts +++ /dev/null @@ -1,117 +0,0 @@ -"use server"; - -import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; -import prisma from "@/db"; -import { revalidatePath } from "next/cache"; -import userFormDataSchema, { - UserFormData, -} from "../formSchemas/userFormSchema"; -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"; -import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; -import hashPassword from "@/modules/auth/utils/hashPassword"; -import db from "@/core/db"; - -/** - * Upserts a user based on the provided UserFormData. - * If the user already exists (determined by `id`), it updates the user; otherwise, it creates a new user. - * Authorization checks are performed based on whether it's a create or update operation. - * - * @param data - The data for creating or updating the user. - * @returns An object containing the success status, message, and any errors. - */ -export default async function upsertUser( - data: UserFormData -): Promise { - try { - const isInsert = !data.id; - - // Authorization check - const permissionType = isInsert ? "users.create" : "users.update"; - if (!(await checkPermission(permissionType))) { - return unauthorized(); - } - - // Validate form data - const validatedFields = userFormDataSchema.safeParse(data); - if (!validatedFields.success) { - throw new DashboardError({ - errorCode: "INVALID_FORM_DATA", - formErrors: mapObjectToFirstValue( - validatedFields.error.flatten().fieldErrors - ), - }); - } - const userData = { - id: validatedFields.data.id ? validatedFields.data.id : undefined, - name: validatedFields.data.name, - photoProfile: validatedFields.data.photoProfileUrl ?? "", - email: validatedFields.data.email, - }; - - const passwordHash = await hashPassword(validatedFields.data.password!); - - const roles = await db.role.findMany({ - where: { - code: { - in: validatedFields.data.roles, - }, - }, - select: { - id: true, // Only select the id field - }, - }); - - // Database operation - if (isInsert) { - if ( - await db.user.findFirst({ - where: { - email: userData.email, - }, - }) - ) { - throw new DashboardError({ - errorCode: "INVALID_FORM_DATA", - formErrors: { - email: "The user is already exists", - }, - }); - } - await db.user.create({ - data: { - ...userData, - passwordHash, - roles: { - connect: roles.map((role) => ({ id: role.id })), - }, - }, - }); - } else { - await db.user.update({ - where: { id: validatedFields.data.id! }, - data: { - ...userData, - roles: { - set: roles.map((role) => ({ id: role.id })), - }, - }, - }); - } - - // Revalidate the cache - revalidatePath("."); - - // Return success message - return { - success: true, - message: `User ${validatedFields.data.name} has been successfully ${ - isInsert ? "created" : "updated" - }.`, - }; - } catch (error) { - return handleCatch(error); - } -} diff --git a/src/modules/userManagement/actions/upsertUserAction.ts b/src/modules/userManagement/actions/upsertUserAction.ts new file mode 100644 index 0000000..ab1d076 --- /dev/null +++ b/src/modules/userManagement/actions/upsertUserAction.ts @@ -0,0 +1,46 @@ +"use server"; + +import { revalidatePath } from "next/cache"; +import { UserFormData } from "../formSchemas/userFormSchema"; +import unauthorized from "@/modules/dashboard/utils/unauthorized"; +import handleCatch from "@/modules/dashboard/utils/handleCatch"; +import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; +import checkPermission from "@/modules/auth/utils/checkPermission"; +import upsertUser from "../services/upsertUser"; + +/** + * Upserts a user based on the provided UserFormData. + * If the user already exists (determined by `id`), it updates the user; otherwise, it creates a new user. + * Authorization checks are performed based on whether it's a create or update operation. + * + * @param data - The data for creating or updating the user. + * @returns An object containing the success status, message, and any errors. + */ +export default async function upsertUserAction( + data: UserFormData +): Promise { + try { + const isInsert = !data.id; + + // Authorization check + const permissionType = isInsert ? "users.create" : "users.update"; + if (!(await checkPermission(permissionType))) { + return unauthorized(); + } + + const user = await upsertUser(data); + + // Revalidate the cache + revalidatePath("."); + + // Return success message + return { + success: true, + message: `User ${user.name} has been successfully ${ + isInsert ? "created" : "updated" + }.`, + }; + } catch (error) { + return handleCatch(error); + } +} diff --git a/src/modules/userManagement/errors/UserManagementError.ts b/src/modules/userManagement/errors/UserManagementError.ts index 269ef27..cf916b2 100644 --- a/src/modules/userManagement/errors/UserManagementError.ts +++ b/src/modules/userManagement/errors/UserManagementError.ts @@ -8,6 +8,7 @@ interface UserManagementErrorOptions { message?: string; errorCode: (typeof UserManagementErrorCodes)[number] | (string & {}); formErrors?: Record + statusCode?: number; } export default class UserManagementError extends DashboardError { @@ -18,6 +19,7 @@ export default class UserManagementError extends DashboardError { super({ errorCode: options.errorCode, message: options.message, + statusCode: options.statusCode, }); this.errorCode = options.errorCode; diff --git a/src/modules/userManagement/modals/UserDeleteModal.tsx b/src/modules/userManagement/modals/UserDeleteModal.tsx index 56ec82a..0f23463 100644 --- a/src/modules/userManagement/modals/UserDeleteModal.tsx +++ b/src/modules/userManagement/modals/UserDeleteModal.tsx @@ -9,7 +9,7 @@ import { } from "@mantine/core"; import { showNotification } from "@/utils/notifications"; import withServerAction from "@/modules/dashboard/utils/withServerAction"; -import deleteUser from "../actions/deleteUser"; +import deleteUserAction from "../actions/deleteUserAction"; import ClientError from "@/core/error/ClientError"; export interface DeleteModalProps { @@ -38,7 +38,7 @@ export default function UserDeleteModal(props: DeleteModalProps) { if (!props.data?.id) return; setSubmitting(true); - withServerAction(() => deleteUser(props.data!.id)) + withServerAction(deleteUserAction, props.data!.id) .then((response) => { showNotification( response.message ?? "User deleted successfully" diff --git a/src/modules/userManagement/modals/UserFormModal.tsx b/src/modules/userManagement/modals/UserFormModal.tsx index 582f096..16980ed 100644 --- a/src/modules/userManagement/modals/UserFormModal.tsx +++ b/src/modules/userManagement/modals/UserFormModal.tsx @@ -21,9 +21,9 @@ import { TbDeviceFloppy } from "react-icons/tb"; import userFormDataSchema, { UserFormData, } from "../formSchemas/userFormSchema"; -import getUserDetailById from "../actions/getUserDetailById"; +import getUserDetailById from "../actions/getUserDetailByIdAction"; import withServerAction from "@/modules/dashboard/utils/withServerAction"; -import upsertUser from "../actions/upsertUser"; +import upsertUserAction from "../actions/upsertUserAction"; import ClientError from "@/core/error/ClientError"; import stringToColorHex from "@/core/utils/stringToColorHex"; import getAllRoles from "@/modules/role/actions/getAllRoles"; @@ -112,7 +112,7 @@ export default function UserFormModal(props: ModalProps) { const handleSubmit = (values: UserFormData) => { setSubmitting(true); - withServerAction(upsertUser, values) + withServerAction(upsertUserAction, values) .then((response) => { showNotification(response.message!, "success"); closeModal(); diff --git a/src/modules/userManagement/services/deleteUser.ts b/src/modules/userManagement/services/deleteUser.ts new file mode 100644 index 0000000..8d9e9f8 --- /dev/null +++ b/src/modules/userManagement/services/deleteUser.ts @@ -0,0 +1,26 @@ +import getCurrentUser from "@/modules/auth/services/getCurrentUser" +import unauthorized from "@/modules/dashboard/utils/unauthorized"; +import "server-only" +import UserManagementError from "../errors/UserManagementError"; +import db from "@/core/db"; +import notFound from "@/modules/dashboard/utils/notFound"; + +export default async function deleteUser(id: string){ + const currentUser = await getCurrentUser(); + + if (!currentUser) return unauthorized(); + + if (currentUser.id !== id) throw new UserManagementError({ + errorCode: "CANNOT_DELETE_SELF", + message: "You cannot delete yourself", + statusCode: 403, + }); + + const user = await db.user.delete({ + where: { id }, + }); + + if (!user) return notFound({message: "The user does not exists"}) + + return true as const; +} \ No newline at end of file diff --git a/src/modules/userManagement/services/getAllUsers.ts b/src/modules/userManagement/services/getAllUsers.ts new file mode 100644 index 0000000..28bf3be --- /dev/null +++ b/src/modules/userManagement/services/getAllUsers.ts @@ -0,0 +1,29 @@ +import db from "@/core/db"; +import "server-only"; + +const getAllUsers = async () => { + const users = await db.user.findMany({ + select: { + id: true, + email: true, + photoProfile: true, + name: true, + roles: { + select: { + name: true, + code: true, + }, + }, + }, + }); + + const result = users.map((user) => ({ + ...user, + photoUrl: user.photoProfile ?? null, + photoProfile: undefined, + })); + + return result; +}; + +export default getAllUsers; diff --git a/src/modules/userManagement/services/getUserById.ts b/src/modules/userManagement/services/getUserById.ts new file mode 100644 index 0000000..ebb7040 --- /dev/null +++ b/src/modules/userManagement/services/getUserById.ts @@ -0,0 +1,24 @@ +import db from "@/core/db"; +import notFound from "@/modules/dashboard/utils/notFound"; + +export default async function getUserById(id: string) { + const user = await db.user.findFirst({ + where: { id }, + select: { + id: true, + email: true, + name: true, + photoProfile: true, + roles: { + select: { + code: true, + name: true, + }, + }, + }, + }); + + if (!user) return notFound({message: "The user does not exists"}) + + return user; +} diff --git a/src/modules/userManagement/services/upsertUser.ts b/src/modules/userManagement/services/upsertUser.ts new file mode 100644 index 0000000..2043644 --- /dev/null +++ b/src/modules/userManagement/services/upsertUser.ts @@ -0,0 +1,80 @@ +import DashboardError from "@/modules/dashboard/errors/DashboardError"; +import userFormDataSchema, { + UserFormData, +} from "../formSchemas/userFormSchema"; +import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue"; +import hashPassword from "@/modules/auth/utils/hashPassword"; +import db from "@/core/db"; +import "server-only" + +export default async function upsertUser(data: UserFormData) { + const isInsert = !data.id; + + // Validate form data + const validatedFields = userFormDataSchema.safeParse(data); + if (!validatedFields.success) { + throw new DashboardError({ + errorCode: "INVALID_FORM_DATA", + formErrors: mapObjectToFirstValue( + validatedFields.error.flatten().fieldErrors + ), + }); + } + const userData = { + id: validatedFields.data.id ? validatedFields.data.id : undefined, + name: validatedFields.data.name, + photoProfile: validatedFields.data.photoProfileUrl ?? "", + email: validatedFields.data.email, + }; + + const passwordHash = await hashPassword(validatedFields.data.password!); + + const roles = await db.role.findMany({ + where: { + code: { + in: validatedFields.data.roles, + }, + }, + select: { + id: true, // Only select the id field + }, + }); + + // Database operation + if (isInsert) { + if ( + await db.user.findFirst({ + where: { + email: userData.email, + }, + }) + ) { + throw new DashboardError({ + errorCode: "INVALID_FORM_DATA", + formErrors: { + email: "The user is already exists", + }, + }); + } + + return await db.user.create({ + data: { + ...userData, + passwordHash, + roles: { + connect: roles.map((role) => ({ id: role.id })), + }, + }, + }); + } else { + return await db.user.update({ + where: { id: validatedFields.data.id! }, + data: { + ...userData, + roles: { + set: roles.map((role) => ({ id: role.id })), + }, + }, + }); + } +} diff --git a/src/modules/userManagement/tables/UsersTable/UsersTable.tsx b/src/modules/userManagement/tables/UsersTable/UsersTable.tsx index d3b44c6..9cdd268 100644 --- a/src/modules/userManagement/tables/UsersTable/UsersTable.tsx +++ b/src/modules/userManagement/tables/UsersTable/UsersTable.tsx @@ -9,12 +9,12 @@ import UserDeleteModal, { DeleteModalProps, } from "../../modals/UserDeleteModal"; import createColumns from "./columns"; -import getAllUsers from "../../actions/getAllUsers"; +import getAllUsers from "../../services/getAllUsers"; import DashboardTable from "@/modules/dashboard/components/DashboardTable"; interface Props { permissions: Partial; - userData: Awaited>; + data: Awaited>; } export default function UsersTable(props: Props) { @@ -32,11 +32,11 @@ export default function UsersTable(props: Props) { }); const userData = useMemo( - () => props.userData.map((data) => ({ + () => props.data.map((data) => ({ ...data, roles: data.roles.map((x) => x.name), })), - [props.userData] + [props.data] ); const table = useReactTable({