Change into action-service pattern

This commit is contained in:
sianida26 2024-03-29 13:38:17 +07:00
parent 443dcdaa36
commit e33a7b5e9c
18 changed files with 280 additions and 242 deletions

View File

@ -1,6 +1,6 @@
import { Card, Stack, Title } from "@mantine/core"; import { Card, Stack, Title } from "@mantine/core";
import React from "react"; import React from "react";
import getUsers from "@/modules/userManagement/actions/getAllUsers"; import getUsers from "@/modules/userManagement/services/getAllUsers";
import { Metadata } from "next"; import { Metadata } from "next";
import UsersTable from "@/modules/userManagement/tables/UsersTable/UsersTable"; import UsersTable from "@/modules/userManagement/tables/UsersTable/UsersTable";
import checkMultiplePermissions from "@/modules/auth/utils/checkMultiplePermissions"; import checkMultiplePermissions from "@/modules/auth/utils/checkMultiplePermissions";
@ -25,7 +25,7 @@ export default async function UsersPage() {
<Stack> <Stack>
<Title order={1}>Users</Title> <Title order={1}>Users</Title>
<Card> <Card>
<UsersTable permissions={permissions} userData={users} /> <UsersTable permissions={permissions} data={users} />
</Card> </Card>
</Stack> </Stack>
); );

View File

@ -14,6 +14,7 @@ interface DashboardErrorOptions {
message?: string; message?: string;
errorCode: (typeof DashboardErrorCodes)[number] | (string & {}); errorCode: (typeof DashboardErrorCodes)[number] | (string & {});
formErrors?: Record<string, string> formErrors?: Record<string, string>
statusCode?: number;
} }
export default class DashboardError extends BaseError { export default class DashboardError extends BaseError {
@ -24,6 +25,7 @@ export default class DashboardError extends BaseError {
super({ super({
errorCode: options.errorCode, errorCode: options.errorCode,
message: options.message, message: options.message,
statusCode: options.statusCode,
}); });
this.errorCode = options.errorCode; this.errorCode = options.errorCode;

View File

@ -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. * Throws a 'NOT_FOUND' DashboardError with a custom or default message.
* @param message Optional custom message for the error. * @param message Optional custom message for the error.
*/ */
const notFound = ({ message }: { message?: string }) => { const notFound = ({ message }: { message?: string }) => {
throw new DashboardError({ throw new BaseError({
errorCode: "NOT_FOUND", errorCode: "NOT_FOUND",
message: message:
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.", "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
}); });
}; };

View File

@ -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<ServerResponseAction> {
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);
}
}

View File

@ -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<ServerResponseAction> {
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);
}
}

View File

@ -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;

View File

@ -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<Awaited<ReturnType<typeof getAllUsers>>>
> {
try {
if (!(await checkPermission("users.readAll"))) unauthorized();
const users = await getAllUsers();
return {
success: true,
data: users,
};
} catch (e) {
return handleCatch(e);
}
}

View File

@ -1,10 +1,8 @@
"use server"; "use server";
import "server-only";
import prisma from "@/db";
import checkPermission from "@/modules/dashboard/services/checkPermission";
import unauthorized from "@/modules/dashboard/utils/unauthorized"; import unauthorized from "@/modules/dashboard/utils/unauthorized";
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction"; 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 = { type UserData = {
id: string; id: string;
@ -23,35 +21,14 @@ type UserData = {
* @param id The unique identifier of the user. * @param id The unique identifier of the user.
* @returns The user's detailed information or an error response. * @returns The user's detailed information or an error response.
*/ */
export default async function getUserDetailById( export default async function getUserDetailByIdAction(
id: string id: string
): Promise<ServerResponseAction<UserData>> { ): Promise<ServerResponseAction<UserData>> {
// Check user permission // Check user permission
if (!checkPermission("users.read")) return unauthorized(); if (!checkPermission("users.read")) return unauthorized();
// Retrieve user data from the database // Retrieve user data from the database
const user = await db.user.findFirst({ const user = await getUserById(id)
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;
// Format user data // Format user data
const formattedUser = { const formattedUser = {
@ -64,7 +41,6 @@ export default async function getUserDetailById(
return { return {
success: true, success: true,
message: "Permission fetched successfully",
data: formattedUser, data: formattedUser,
} as const; } as const;
} }

View File

@ -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<ServerResponseAction> {
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);
}
}

View File

@ -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<ServerResponseAction> {
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);
}
}

View File

@ -8,6 +8,7 @@ interface UserManagementErrorOptions {
message?: string; message?: string;
errorCode: (typeof UserManagementErrorCodes)[number] | (string & {}); errorCode: (typeof UserManagementErrorCodes)[number] | (string & {});
formErrors?: Record<string, string> formErrors?: Record<string, string>
statusCode?: number;
} }
export default class UserManagementError extends DashboardError { export default class UserManagementError extends DashboardError {
@ -18,6 +19,7 @@ export default class UserManagementError extends DashboardError {
super({ super({
errorCode: options.errorCode, errorCode: options.errorCode,
message: options.message, message: options.message,
statusCode: options.statusCode,
}); });
this.errorCode = options.errorCode; this.errorCode = options.errorCode;

View File

@ -9,7 +9,7 @@ import {
} from "@mantine/core"; } from "@mantine/core";
import { showNotification } from "@/utils/notifications"; import { showNotification } from "@/utils/notifications";
import withServerAction from "@/modules/dashboard/utils/withServerAction"; import withServerAction from "@/modules/dashboard/utils/withServerAction";
import deleteUser from "../actions/deleteUser"; import deleteUserAction from "../actions/deleteUserAction";
import ClientError from "@/core/error/ClientError"; import ClientError from "@/core/error/ClientError";
export interface DeleteModalProps { export interface DeleteModalProps {
@ -38,7 +38,7 @@ export default function UserDeleteModal(props: DeleteModalProps) {
if (!props.data?.id) return; if (!props.data?.id) return;
setSubmitting(true); setSubmitting(true);
withServerAction(() => deleteUser(props.data!.id)) withServerAction(deleteUserAction, props.data!.id)
.then((response) => { .then((response) => {
showNotification( showNotification(
response.message ?? "User deleted successfully" response.message ?? "User deleted successfully"

View File

@ -21,9 +21,9 @@ import { TbDeviceFloppy } from "react-icons/tb";
import userFormDataSchema, { import userFormDataSchema, {
UserFormData, UserFormData,
} from "../formSchemas/userFormSchema"; } from "../formSchemas/userFormSchema";
import getUserDetailById from "../actions/getUserDetailById"; import getUserDetailById from "../actions/getUserDetailByIdAction";
import withServerAction from "@/modules/dashboard/utils/withServerAction"; import withServerAction from "@/modules/dashboard/utils/withServerAction";
import upsertUser from "../actions/upsertUser"; import upsertUserAction from "../actions/upsertUserAction";
import ClientError from "@/core/error/ClientError"; import ClientError from "@/core/error/ClientError";
import stringToColorHex from "@/core/utils/stringToColorHex"; import stringToColorHex from "@/core/utils/stringToColorHex";
import getAllRoles from "@/modules/role/actions/getAllRoles"; import getAllRoles from "@/modules/role/actions/getAllRoles";
@ -112,7 +112,7 @@ export default function UserFormModal(props: ModalProps) {
const handleSubmit = (values: UserFormData) => { const handleSubmit = (values: UserFormData) => {
setSubmitting(true); setSubmitting(true);
withServerAction(upsertUser, values) withServerAction(upsertUserAction, values)
.then((response) => { .then((response) => {
showNotification(response.message!, "success"); showNotification(response.message!, "success");
closeModal(); closeModal();

View File

@ -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;
}

View File

@ -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;

View File

@ -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;
}

View File

@ -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 })),
},
},
});
}
}

View File

@ -9,12 +9,12 @@ import UserDeleteModal, {
DeleteModalProps, DeleteModalProps,
} from "../../modals/UserDeleteModal"; } from "../../modals/UserDeleteModal";
import createColumns from "./columns"; import createColumns from "./columns";
import getAllUsers from "../../actions/getAllUsers"; import getAllUsers from "../../services/getAllUsers";
import DashboardTable from "@/modules/dashboard/components/DashboardTable"; import DashboardTable from "@/modules/dashboard/components/DashboardTable";
interface Props { interface Props {
permissions: Partial<CrudPermissions>; permissions: Partial<CrudPermissions>;
userData: Awaited<ReturnType<typeof getAllUsers>>; data: Awaited<ReturnType<typeof getAllUsers>>;
} }
export default function UsersTable(props: Props) { export default function UsersTable(props: Props) {
@ -32,11 +32,11 @@ export default function UsersTable(props: Props) {
}); });
const userData = useMemo( const userData = useMemo(
() => props.userData.map((data) => ({ () => props.data.map((data) => ({
...data, ...data,
roles: data.roles.map((x) => x.name), roles: data.roles.map((x) => x.name),
})), })),
[props.userData] [props.data]
); );
const table = useReactTable({ const table = useReactTable({