Change role into action-service pattern

This commit is contained in:
sianida26 2024-03-29 23:15:17 +07:00
parent e33a7b5e9c
commit b94f2c6aad
16 changed files with 267 additions and 280 deletions

View File

@ -1,6 +1,6 @@
import checkMultiplePermissions from "@/modules/auth/utils/checkMultiplePermissions";
import unauthorized from "@/modules/dashboard/utils/unauthorized";
import getAllRoles from "@/modules/role/actions/getAllRoles";
import getAllRoles from "@/modules/role/services/getAllRoles";
import RolesTable from "@/modules/role/tables/RolesTable/RolesTable";
import { Card, Stack, Title } from "@mantine/core";
import { Metadata } from "next";
@ -21,14 +21,13 @@ export default async function RolesPage() {
if (!permissions.readAll) unauthorized()
const res = await getAllRoles();
if (!res.success) throw new Error("Error while fetch roles");
const roles = await getAllRoles();
return (
<Stack>
<Title order={1}>Roles</Title>
<Card>
<RolesTable permissions={permissions} roles={res.data} />
<RolesTable permissions={permissions} data={roles} />
</Card>
</Stack>
);

View File

@ -1,45 +0,0 @@
import getCurrentUser from "@/modules/auth/utils/getCurrentUser";
import "server-only";
/**
* Checks if the current user has the specified permissions.
*
* Deprecated. Use `checkPermission()` from auth module instead.
*
* @deprecated
* @param permission - The specific permission to check. If it's "guest-only", the function returns true if the user is not authenticated. If it's "authenticated-only", it returns true if the user is authenticated. For other permissions, it checks against the user's roles and direct permissions.
* @param currentUser - Optional. The current user object. If not provided, the function retrieves the current user.
* @returns true if the user has the required permission, otherwise false.
*/
export default async function checkPermission(
permission?: "guest-only" | "authenticated-only" | (string & {}),
currentUser?: Awaited<ReturnType<typeof getCurrentUser>>
): Promise<boolean> {
// Allow if no specific permission is required.
if (!permission) return true;
// Retrieve current user if not provided.
const user = currentUser ?? (await getCurrentUser());
// Handle non-authenticated users.
if (!user) {
return permission === "guest-only";
}
// Allow authenticated users if the permission is 'authenticated-only'.
if (permission === "authenticated-only") {
return true;
}
// Short-circuit for super-admin role to allow all permissions.
if (user.roles.some((role) => role.code === "super-admin")) return true;
// Aggregate all role codes and direct permissions into a set for efficient lookup.
const permissions = new Set<string>([
...user.roles.map((role) => role.code),
...user.directPermissions.map((dp) => dp.code),
]);
// Check if the user has the required permission.
return permissions.has(permission);
}

View File

@ -1,22 +1,19 @@
"use server";
import db from "@/core/db";
import prisma from "@/db";
import checkPermission from "@/modules/dashboard/services/checkPermission";
import checkPermission from "@/modules/auth/utils/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";
import { notFound } from "next/navigation";
import deleteRole from "../services/deleteRole";
export default async function deleteRole(
export default async function deleteRoleAction(
id: string
): Promise<ServerResponseAction> {
try {
if (!(await checkPermission("roles.delete"))) return unauthorized();
const role = await db.role.delete({
where: { id },
});
await deleteRole(id);
revalidatePath(".");

View File

@ -1,58 +0,0 @@
"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 Role from "../types/Role";
import db from "@/core/db";
/**
* 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 getAllRoles(): Promise<
ServerResponseAction<Role[]>
> {
try {
// Authorization check
if (!(await checkPermission("roles.getAll"))) {
unauthorized();
}
// Fetch roles from the database
const roles = await db.role.findMany({
include: {
_count: {
select: {
permissions: true,
users: true,
},
},
},
});
// Transform the data into the desired format
const result = roles.map(
({ id, code, name, description, isActive, _count }) => ({
id,
code,
name,
description,
isActive,
permissionCount: _count.permissions,
userCount: _count.users,
})
);
return {
success: true,
data: result
}
} catch (error) {
return handleCatch(error)
}
}

View File

@ -0,0 +1,34 @@
"use server";
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
import handleCatch from "@/modules/dashboard/utils/handleCatch";
import unauthorized from "@/modules/dashboard/utils/unauthorized";
import Role from "../types/Role";
import checkPermission from "@/modules/auth/utils/checkPermission";
import getAllRoles from "../services/getAllRoles";
/**
* 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 getAllRolesAction(): Promise<
ServerResponseAction<Role[]>
> {
try {
// Authorization check
if (!(await checkPermission("roles.readAll"))) {
return unauthorized();
}
// Fetch roles from the database
const roles = await getAllRoles()
return {
success: true,
data: roles
}
} catch (error) {
return handleCatch(error)
}
}

View File

@ -1,58 +0,0 @@
"use server";
import db from "@/core/db";
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";
type RoleData = {
id: string;
code: string;
name: string;
description: string;
isActive: boolean;
permissions: {
id: string;
code: string;
name: string;
}[]
}
export default async function getRoleById(id: string): Promise<ServerResponseAction<RoleData>>{
try{
if (!(await checkPermission("roles.read"))) return unauthorized();
const role = await db.role.findFirst({
where: { id },
select: {
code: true,
description: true,
id: true,
isActive: true,
name: true,
permissions: {
select: {
id: true,
code: true,
name: true,
},
},
},
});
if (!role) {
throw new Error("Permission not found")
}
return {
success: true,
message: "Role fetched successfully",
data: role,
};
} catch (e){
return handleCatch(e)
}
}

View File

@ -0,0 +1,24 @@
"use server";
import checkPermission from "@/modules/auth/utils/checkPermission";
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
import handleCatch from "@/modules/dashboard/utils/handleCatch";
import unauthorized from "@/modules/dashboard/utils/unauthorized";
import getRoleById from "../services/getRoleById";
export default async function getRoleByIdAction(id: string): Promise<ServerResponseAction<Awaited<ReturnType<typeof getRoleById>>>>{
try{
if (!(await checkPermission("roles.read"))) return unauthorized();
const role = await getRoleById(id)
return {
success: true,
message: "Role fetched successfully",
data: role,
};
} catch (e){
return handleCatch(e)
}
}

View File

@ -1,99 +0,0 @@
"use server";
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
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";
import db from "@/core/db";
/**
* 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 data - The data for creating or updating the role.
* @returns An object containing the success status, message, and any errors.
*/
export default async function upsertRole(
data: RoleFormData
): Promise<ServerResponseAction> {
try {
const isInsert = !data.id;
// 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) {
throw new DashboardError({
errorCode: "INVALID_FORM_DATA",
formErrors: mapObjectToFirstValue(
validatedFields.error.flatten().fieldErrors
),
});
}
const roleData = {
code: validatedFields.data.code,
description: validatedFields.data.description,
name: validatedFields.data.name,
isActive: validatedFields.data.isActive,
};
const permissionIds = validatedFields.data.permissions.map(
(permission) => ({ code: permission })
);
// Database operation
if (isInsert) {
if (
await db.role.findFirst({
where: {
code: roleData.code,
},
})
) {
throw new DashboardError({
errorCode: "INVALID_FORM_DATA",
formErrors: {
code: "The code is already exists",
},
});
}
await db.role.create({
data: {
...roleData,
permissions: {
connect: permissionIds,
},
},
});
} else {
await db.role.update({
where: { id: validatedFields.data.id! },
data: { ...roleData, permissions: { connect: permissionIds } },
});
}
// Revalidate the cache
revalidatePath(".");
// Return success message
return {
success: true,
message: `Role ${validatedFields.data.name} has been successfully ${
isInsert ? "created" : "updated"
}.`,
};
} catch (error) {
return handleCatch(error);
}
}

View File

@ -0,0 +1,50 @@
"use server";
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
import { revalidatePath } from "next/cache";
import ServerResponseAction from "@/modules/dashboard/types/ServerResponseAction";
import unauthorized from "@/modules/dashboard/utils/unauthorized";
import DashboardError from "@/modules/dashboard/errors/DashboardError";
import handleCatch from "@/modules/dashboard/utils/handleCatch";
import db from "@/core/db";
import checkPermission from "@/modules/auth/utils/checkPermission";
import upsertRole from "../services/upsertRole";
/**
* 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 data - The data for creating or updating the role.
* @returns An object containing the success status, message, and any errors.
*/
export default async function upsertRoleAction(
data: RoleFormData
): Promise<ServerResponseAction> {
try {
const isInsert = !data.id;
// Authorization check
const permissionType = isInsert ? "roles.create" : "roles.update";
if (!(await checkPermission(permissionType))) {
return unauthorized();
}
// Validate form data
const result = await upsertRole(data);
// Revalidate the cache
revalidatePath(".");
// Return success message
return {
success: true,
message: `Role ${result.name} has been successfully ${
isInsert ? "created" : "updated"
}.`,
};
} catch (error) {
return handleCatch(error);
}
}

View File

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

View File

@ -21,8 +21,8 @@ import { TbDeviceFloppy } from "react-icons/tb";
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
import getAllPermissions from "@/modules/permission/actions/getAllPermissions";
import withServerAction from "@/modules/dashboard/utils/withServerAction";
import getRoleById from "../actions/getRoleById";
import upsertRole from "../actions/upsertRole";
import getRoleByIdAction from "../actions/getRoleByIdAction";
import upsertRoleAction from "../actions/upsertRoleAction";
import ClientError from "@/core/error/ClientError";
export interface ModalProps {
@ -96,7 +96,7 @@ export default function FormModal(props: ModalProps) {
}
setFetching(true);
withServerAction(getRoleById, props.id)
withServerAction(getRoleByIdAction, props.id)
.then((response) => {
const data = response.data;
form.setValues({
@ -133,7 +133,7 @@ export default function FormModal(props: ModalProps) {
const handleSubmit = (values: RoleFormData) => {
setSubmitting(true);
withServerAction(upsertRole, values)
withServerAction(upsertRoleAction, values)
.then((response) => {
showNotification(response.message!, "success");
closeModal();

View File

@ -0,0 +1,12 @@
import db from "@/core/db";
import notFound from "@/modules/dashboard/utils/notFound";
export default async function deleteRole(id: string) {
const role = await db.role.delete({
where: { id },
});
if (!role) notFound({message: "The role doesn't exists"})
return true as const;
}

View File

@ -0,0 +1,32 @@
import db from "@/core/db";
import "server-only"
import Role from "../types/Role";
export default async function getAllRoles(): Promise<Role[]>{
const roles = await db.role.findMany({
include: {
_count: {
select: {
permissions: true,
users: true,
},
},
},
});
//data transformation
const result = roles.map(
({ id, code, name, description, isActive, _count }) => ({
id,
code,
name,
description,
isActive,
permissionCount: _count.permissions,
userCount: _count.users,
})
);
return result;
}

View File

@ -0,0 +1,40 @@
import "server-only";
import db from "@/core/db";
import notFound from "@/modules/dashboard/utils/notFound";
type RoleData = {
id: string;
code: string;
name: string;
description: string;
isActive: boolean;
permissions: {
id: string;
code: string;
name: string;
}[];
};
export default async function getRoleById(id: string): Promise<RoleData> {
const role = await db.role.findFirst({
where: { id },
select: {
code: true,
description: true,
id: true,
isActive: true,
name: true,
permissions: {
select: {
id: true,
code: true,
name: true,
},
},
},
});
if (!role) return notFound({ message: "Role doesn't exists" });
return role;
}

View File

@ -0,0 +1,59 @@
import DashboardError from "@/modules/dashboard/errors/DashboardError";
import roleFormDataSchema, { RoleFormData } from "../formSchemas/RoleFormData";
import mapObjectToFirstValue from "@/utils/mapObjectToFirstValue";
import db from "@/core/db";
export default async function upsertRole(data: RoleFormData) {
const isInsert = !data.id;
const validatedFields = roleFormDataSchema.safeParse(data);
if (!validatedFields.success) {
throw new DashboardError({
errorCode: "INVALID_FORM_DATA",
formErrors: mapObjectToFirstValue(
validatedFields.error.flatten().fieldErrors
),
});
}
const roleData = {
code: validatedFields.data.code,
description: validatedFields.data.description,
name: validatedFields.data.name,
isActive: validatedFields.data.isActive,
};
const permissionIds = validatedFields.data.permissions.map(
(permission) => ({ code: permission })
);
// Database operation
if (isInsert) {
if (
await db.role.findFirst({
where: {
code: roleData.code,
},
})
) {
throw new DashboardError({
errorCode: "INVALID_FORM_DATA",
formErrors: {
code: "The code is already exists",
},
});
}
return await db.role.create({
data: {
...roleData,
permissions: {
connect: permissionIds,
},
},
});
} else {
return await db.role.update({
where: { id: validatedFields.data.id! },
data: { ...roleData, permissions: { connect: permissionIds } },
});
}
}

View File

@ -12,7 +12,7 @@ import Role from "../../types/Role";
interface Props {
permissions: Partial<CrudPermissions>;
roles: Role[];
data: Role[];
}
export default function RolesTable(props: Props) {
@ -28,7 +28,7 @@ export default function RolesTable(props: Props) {
});
const table = useReactTable({
data: props.roles,
data: props.data,
columns: createColumns({
permissions: props.permissions,
actions: {