diff --git a/src/app/dashboard/roles/page.tsx b/src/app/dashboard/roles/page.tsx index 59ec619..bd9d1a6 100644 --- a/src/app/dashboard/roles/page.tsx +++ b/src/app/dashboard/roles/page.tsx @@ -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 ( Roles - + ); diff --git a/src/modules/dashboard/services/checkPermission.ts b/src/modules/dashboard/services/checkPermission.ts deleted file mode 100644 index a75a6c5..0000000 --- a/src/modules/dashboard/services/checkPermission.ts +++ /dev/null @@ -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> -): Promise { - // 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([ - ...user.roles.map((role) => role.code), - ...user.directPermissions.map((dp) => dp.code), - ]); - - // Check if the user has the required permission. - return permissions.has(permission); -} diff --git a/src/modules/role/actions/deleteRole.ts b/src/modules/role/actions/deleteRoleAction.ts similarity index 67% rename from src/modules/role/actions/deleteRole.ts rename to src/modules/role/actions/deleteRoleAction.ts index 2f37fc3..ef3998d 100644 --- a/src/modules/role/actions/deleteRole.ts +++ b/src/modules/role/actions/deleteRoleAction.ts @@ -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 { try { if (!(await checkPermission("roles.delete"))) return unauthorized(); - const role = await db.role.delete({ - where: { id }, - }); + + await deleteRole(id); revalidatePath("."); diff --git a/src/modules/role/actions/getAllRoles.ts b/src/modules/role/actions/getAllRoles.ts deleted file mode 100644 index c3b52e6..0000000 --- a/src/modules/role/actions/getAllRoles.ts +++ /dev/null @@ -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 -> { - 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) - } -} diff --git a/src/modules/role/actions/getAllRolesAction.ts b/src/modules/role/actions/getAllRolesAction.ts new file mode 100644 index 0000000..2493fe3 --- /dev/null +++ b/src/modules/role/actions/getAllRolesAction.ts @@ -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 +> { + 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) + } +} diff --git a/src/modules/role/actions/getRoleById.ts b/src/modules/role/actions/getRoleById.ts deleted file mode 100644 index 1351cbf..0000000 --- a/src/modules/role/actions/getRoleById.ts +++ /dev/null @@ -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>{ - 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) - } -} diff --git a/src/modules/role/actions/getRoleByIdAction.ts b/src/modules/role/actions/getRoleByIdAction.ts new file mode 100644 index 0000000..cfd68b7 --- /dev/null +++ b/src/modules/role/actions/getRoleByIdAction.ts @@ -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>>>{ + 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) + } +} diff --git a/src/modules/role/actions/upsertRole.ts b/src/modules/role/actions/upsertRole.ts deleted file mode 100644 index 0bc1533..0000000 --- a/src/modules/role/actions/upsertRole.ts +++ /dev/null @@ -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 { - 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); - } -} diff --git a/src/modules/role/actions/upsertRoleAction.ts b/src/modules/role/actions/upsertRoleAction.ts new file mode 100644 index 0000000..e2c51e1 --- /dev/null +++ b/src/modules/role/actions/upsertRoleAction.ts @@ -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 { + 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); + } +} diff --git a/src/modules/role/modals/DeleteModal.tsx b/src/modules/role/modals/DeleteModal.tsx index 82a0dea..872b3f7 100644 --- a/src/modules/role/modals/DeleteModal.tsx +++ b/src/modules/role/modals/DeleteModal.tsx @@ -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" diff --git a/src/modules/role/modals/FormModal.tsx b/src/modules/role/modals/FormModal.tsx index 49b57b2..4dc5202 100644 --- a/src/modules/role/modals/FormModal.tsx +++ b/src/modules/role/modals/FormModal.tsx @@ -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(); diff --git a/src/modules/role/services/deleteRole.ts b/src/modules/role/services/deleteRole.ts new file mode 100644 index 0000000..104df52 --- /dev/null +++ b/src/modules/role/services/deleteRole.ts @@ -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; +} \ No newline at end of file diff --git a/src/modules/role/services/getAllRoles.ts b/src/modules/role/services/getAllRoles.ts new file mode 100644 index 0000000..0a96a66 --- /dev/null +++ b/src/modules/role/services/getAllRoles.ts @@ -0,0 +1,32 @@ +import db from "@/core/db"; +import "server-only" +import Role from "../types/Role"; + +export default async function getAllRoles(): Promise{ + + 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; +} \ No newline at end of file diff --git a/src/modules/role/services/getRoleById.ts b/src/modules/role/services/getRoleById.ts new file mode 100644 index 0000000..753cd38 --- /dev/null +++ b/src/modules/role/services/getRoleById.ts @@ -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 { + 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; +} diff --git a/src/modules/role/services/upsertRole.ts b/src/modules/role/services/upsertRole.ts new file mode 100644 index 0000000..7a737cd --- /dev/null +++ b/src/modules/role/services/upsertRole.ts @@ -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 } }, + }); + } +} diff --git a/src/modules/role/tables/RolesTable/RolesTable.tsx b/src/modules/role/tables/RolesTable/RolesTable.tsx index 45883f8..6131cca 100644 --- a/src/modules/role/tables/RolesTable/RolesTable.tsx +++ b/src/modules/role/tables/RolesTable/RolesTable.tsx @@ -12,7 +12,7 @@ import Role from "../../types/Role"; interface Props { permissions: Partial; - 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: {