From 36afde8aa49f4d7f985c4e9fe1d0838757a3fda7 Mon Sep 17 00:00:00 2001 From: Tiago Ribeiro Date: Thu, 6 Feb 2025 18:48:31 +0000 Subject: [PATCH 1/3] Created the new permissions as an example --- src/pages/approval-workflows/index.tsx | 687 +++++++++++------------ src/pages/entities/[id]/roles/[role].tsx | 31 + src/resources/entityPermissions.ts | 10 +- 3 files changed, 368 insertions(+), 360 deletions(-) diff --git a/src/pages/approval-workflows/index.tsx b/src/pages/approval-workflows/index.tsx index fbd84ed6..84b65c1c 100644 --- a/src/pages/approval-workflows/index.tsx +++ b/src/pages/approval-workflows/index.tsx @@ -4,414 +4,383 @@ import Button from "@/components/Low/Button"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import useApprovalWorkflows from "@/hooks/useApprovalWorkflows"; -import { Module, ModuleTypeLabels } from "@/interfaces"; -import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow"; -import { Entity, EntityWithRoles } from "@/interfaces/entity"; -import { User } from "@/interfaces/user"; -import { sessionOptions } from "@/lib/session"; -import { redirect, serialize } from "@/utils"; -import { requestUser } from "@/utils/api"; -import { getApprovalWorkflows } from "@/utils/approval.workflows.be"; -import { getEntities } from "@/utils/entities.be"; -import { shouldRedirectHome } from "@/utils/navigation.disabled"; -import { getSpecificUsers } from "@/utils/users.be"; -import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; +import {useAllowedEntities, useAllowedEntitiesSomePermissions, useEntityPermission} from "@/hooks/useEntityPermissions"; +import {Module, ModuleTypeLabels} from "@/interfaces"; +import {ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel} from "@/interfaces/approval.workflow"; +import {Entity, EntityWithRoles} from "@/interfaces/entity"; +import {User} from "@/interfaces/user"; +import {sessionOptions} from "@/lib/session"; +import {redirect, serialize} from "@/utils"; +import {requestUser} from "@/utils/api"; +import {getApprovalWorkflows} from "@/utils/approval.workflows.be"; +import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; +import {shouldRedirectHome} from "@/utils/navigation.disabled"; +import {findAllowedEntities} from "@/utils/permissions"; +import {getSpecificUsers} from "@/utils/users.be"; +import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import { withIronSessionSsr } from "iron-session/next"; +import {withIronSessionSsr} from "iron-session/next"; import Head from "next/head"; import Link from "next/link"; -import { useEffect, useState } from "react"; -import { BsTrash } from "react-icons/bs"; -import { FaRegEdit } from "react-icons/fa"; -import { IoIosAddCircleOutline } from "react-icons/io"; -import { toast, ToastContainer } from "react-toastify"; +import {useEffect, useState} from "react"; +import {BsTrash} from "react-icons/bs"; +import {FaRegEdit} from "react-icons/fa"; +import {IoIosAddCircleOutline} from "react-icons/io"; +import {toast, ToastContainer} from "react-toastify"; const columnHelper = createColumnHelper(); -export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { - const user = await requestUser(req, res) - if (!user) return redirect("/login") +export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { + const user = await requestUser(req, res); + if (!user) return redirect("/login"); - if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) - return redirect("/") + if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/"); - const workflows = await getApprovalWorkflows("active-workflows"); + const workflows = await getApprovalWorkflows("active-workflows"); - const allAssigneeIds: string[] = [ - ...new Set( - workflows - .map(workflow => workflow.steps - .map(step => step.assignees) - .flat() - ).flat() - ) - ]; + const allAssigneeIds: string[] = [...new Set(workflows.map((workflow) => workflow.steps.map((step) => step.assignees).flat()).flat())]; - return { - props: serialize({ - user, - initialWorkflows: workflows, - workflowsAssignees: await getSpecificUsers(allAssigneeIds), - userEntitiesWithLabel: await getEntities(user.entities.map(entity => entity.id)), - }), - }; + const entities = await getEntitiesWithRoles(user.entities.map((entity) => entity.id)); + const allowedEntities = findAllowedEntities(user, entities, "view_workflows"); + + return { + props: serialize({ + user, + initialWorkflows: workflows, + workflowsAssignees: await getSpecificUsers(allAssigneeIds), + userEntitiesWithLabel: entities, + }), + }; }, sessionOptions); -const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = { - approved: "bg-green-100 text-green-800 border border-green-300 before:content-[''] before:w-2 before:h-2 before:bg-green-500 before:rounded-full before:inline-block before:mr-2", - pending: "bg-orange-100 text-orange-800 border border-orange-300 before:content-[''] before:w-2 before:h-2 before:bg-orange-500 before:rounded-full before:inline-block before:mr-2", - rejected: "bg-red-100 text-red-800 border border-red-300 before:content-[''] before:w-2 before:h-2 before:bg-red-500 before:rounded-full before:inline-block before:mr-2", +const StatusClassNames: {[key in ApprovalWorkflowStatus]: string} = { + approved: + "bg-green-100 text-green-800 border border-green-300 before:content-[''] before:w-2 before:h-2 before:bg-green-500 before:rounded-full before:inline-block before:mr-2", + pending: + "bg-orange-100 text-orange-800 border border-orange-300 before:content-[''] before:w-2 before:h-2 before:bg-orange-500 before:rounded-full before:inline-block before:mr-2", + rejected: + "bg-red-100 text-red-800 border border-red-300 before:content-[''] before:w-2 before:h-2 before:bg-red-500 before:rounded-full before:inline-block before:mr-2", }; type CustomStatus = ApprovalWorkflowStatus | undefined; type CustomEntity = EntityWithRoles["id"] | undefined; const STATUS_OPTIONS = [ - { - label: "Approved", - value: "approved", - filter: (x: ApprovalWorkflow) => x.status === "approved", - }, - { - label: "Pending", - value: "pending", - filter: (x: ApprovalWorkflow) => x.status === "pending", - }, - { - label: "Rejected", - value: "rejected", - filter: (x: ApprovalWorkflow) => x.status === "rejected", - }, + { + label: "Approved", + value: "approved", + filter: (x: ApprovalWorkflow) => x.status === "approved", + }, + { + label: "Pending", + value: "pending", + filter: (x: ApprovalWorkflow) => x.status === "pending", + }, + { + label: "Rejected", + value: "rejected", + filter: (x: ApprovalWorkflow) => x.status === "rejected", + }, ]; interface Props { - user: User, - initialWorkflows: ApprovalWorkflow[], - workflowsAssignees: User[], - userEntitiesWithLabel: Entity[], + user: User; + initialWorkflows: ApprovalWorkflow[]; + workflowsAssignees: User[]; + userEntitiesWithLabel: EntityWithRoles[]; } -export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAssignees, userEntitiesWithLabel }: Props) { +export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssignees, userEntitiesWithLabel}: Props) { + const {workflows, reload} = useApprovalWorkflows(); + const currentWorkflows = workflows || initialWorkflows; - const { workflows, reload } = useApprovalWorkflows(); - const currentWorkflows = workflows || initialWorkflows; + const [filteredWorkflows, setFilteredWorkflows] = useState([]); - const [filteredWorkflows, setFilteredWorkflows] = useState([]); + const [statusFilter, setStatusFilter] = useState(undefined); + const [entityFilter, setEntityFilter] = useState(undefined); + const [nameFilter, setNameFilter] = useState(""); - const [statusFilter, setStatusFilter] = useState(undefined); - const [entityFilter, setEntityFilter] = useState(undefined); - const [nameFilter, setNameFilter] = useState(""); + const allowedEntities = useAllowedEntities(user, userEntitiesWithLabel, "view_workflows"); + const allowedSomeEntities = useAllowedEntitiesSomePermissions(user, userEntitiesWithLabel, ["view_workflows", "create_workflow"]); - const ENTITY_OPTIONS = [ - ...userEntitiesWithLabel - .map(entity => ({ - label: entity.label, - value: entity.id, - filter: (x: ApprovalWorkflow) => x.entityId === entity.id, - })) - .sort((a, b) => a.label.localeCompare(b.label)), - ]; + const ENTITY_OPTIONS = [ + ...userEntitiesWithLabel + .map((entity) => ({ + label: entity.label, + value: entity.id, + filter: (x: ApprovalWorkflow) => x.entityId === entity.id, + })) + .sort((a, b) => a.label.localeCompare(b.label)), + ]; - useEffect(() => { - const filters: Array<(workflow: ApprovalWorkflow) => boolean> = []; + useEffect(() => { + const filters: Array<(workflow: ApprovalWorkflow) => boolean> = []; - if (statusFilter && statusFilter !== undefined) { - const statusOption = STATUS_OPTIONS.find((x) => x.value === statusFilter); - if (statusOption && statusOption.filter) { - filters.push(statusOption.filter); - } - } + if (statusFilter && statusFilter !== undefined) { + const statusOption = STATUS_OPTIONS.find((x) => x.value === statusFilter); + if (statusOption && statusOption.filter) { + filters.push(statusOption.filter); + } + } - if (entityFilter && entityFilter !== undefined) { - const entityOption = ENTITY_OPTIONS.find((x) => x.value === entityFilter); - if (entityOption && entityOption.filter) { - filters.push(entityOption.filter); - } - } + if (entityFilter && entityFilter !== undefined) { + const entityOption = ENTITY_OPTIONS.find((x) => x.value === entityFilter); + if (entityOption && entityOption.filter) { + filters.push(entityOption.filter); + } + } - if (nameFilter.trim() !== "") { - const nameFilterFunction = (workflow: ApprovalWorkflow) => - workflow.name.toLowerCase().includes(nameFilter.toLowerCase()); - filters.push(nameFilterFunction); - } + if (nameFilter.trim() !== "") { + const nameFilterFunction = (workflow: ApprovalWorkflow) => workflow.name.toLowerCase().includes(nameFilter.toLowerCase()); + filters.push(nameFilterFunction); + } - // Apply all filters - const filtered = currentWorkflows.filter(workflow => filters.every(filterFn => filterFn(workflow))); - setFilteredWorkflows(filtered); - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [currentWorkflows, statusFilter, entityFilter, nameFilter]); + // Apply all filters + const filtered = currentWorkflows.filter((workflow) => filters.every((filterFn) => filterFn(workflow))); + setFilteredWorkflows(filtered); + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [currentWorkflows, statusFilter, entityFilter, nameFilter]); + const handleNameFilterChange = (name: ApprovalWorkflow["name"]) => { + setNameFilter(name); + }; - const handleNameFilterChange = (name: ApprovalWorkflow["name"]) => { - setNameFilter(name); - }; + const deleteApprovalWorkflow = (id: string | undefined, name: string) => { + if (id === undefined) return; + if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; - const deleteApprovalWorkflow = (id: string | undefined, name: string) => { - if (id === undefined) return; - if (!confirm(`Are you sure you want to delete this Approval Workflow?`)) return; + axios + .delete(`/api/approval-workflows/${id}`) + .then(() => { + toast.success(`Successfully deleted ${name} Approval Workflow.`); + reload(); + }) + .catch((reason) => { + if (reason.response.status === 404) { + toast.error("Approval Workflow not found!"); + } else if (reason.response.status === 403) { + toast.error("You do not have permission to delete an Approval Workflow!"); + } else { + toast.error("Something went wrong, please try again later."); + } + return; + }); + }; - axios - .delete(`/api/approval-workflows/${id}`) - .then(() => { - toast.success(`Successfully deleted ${name} Approval Workflow.`); - reload(); - }) - .catch((reason) => { - if (reason.response.status === 404) { - toast.error("Approval Workflow not found!"); - } else if (reason.response.status === 403) { - toast.error("You do not have permission to delete an Approval Workflow!"); - } else { - toast.error("Something went wrong, please try again later."); - } - return; - }) - }; + const columns = [ + columnHelper.accessor("name", { + header: "EXAM NAME", + cell: (info) => {info.getValue()}, + }), + columnHelper.accessor("modules", { + header: "MODULES", + cell: (info) => ( +
+ {info.getValue().map((module: Module, index: number) => ( + + {ModuleTypeLabels[module]} + + ))} +
+ ), + }), + columnHelper.accessor("status", { + header: "STATUS", + cell: (info) => ( + + {ApprovalWorkflowStatusLabel[info.getValue()]} + + ), + }), + columnHelper.accessor("entityId", { + header: "ENTITY", + cell: (info) => {userEntitiesWithLabel.find((entity) => entity.id === info.getValue())?.label}, + }), + columnHelper.accessor("steps", { + id: "currentAssignees", + header: "CURRENT ASSIGNEES", + cell: (info) => { + const steps = info.row.original.steps; + const currentStep = steps.find((step) => !step.completed); + const rejected = steps.find((step) => step.rejected); - const columns = [ - columnHelper.accessor("name", { - header: "EXAM NAME", - cell: (info) => ( - - {info.getValue()} - - ), - }), - columnHelper.accessor("modules", { - header: "MODULES", - cell: (info) => ( -
- {info.getValue().map((module: Module, index: number) => ( - - {ModuleTypeLabels[module]} - - ))} -
- ), - }), - columnHelper.accessor("status", { - header: "STATUS", - cell: (info) => ( - - {ApprovalWorkflowStatusLabel[info.getValue()]} - - ), - }), - columnHelper.accessor("entityId", { - header: "ENTITY", - cell: (info) => ( - - {userEntitiesWithLabel.find((entity) => entity.id === info.getValue())?.label} - - ), - }), - columnHelper.accessor("steps", { - id: "currentAssignees", - header: "CURRENT ASSIGNEES", - cell: (info) => { - const steps = info.row.original.steps; - const currentStep = steps.find((step) => !step.completed); - const rejected = steps.find((step) => step.rejected); + if (rejected) return ""; - if (rejected) return ""; + const assignees = currentStep?.assignees.map((assigneeId) => { + const assignee = workflowsAssignees.find((user) => user.id === assigneeId); + return assignee?.name || "Unknown Assignee"; + }); - const assignees = currentStep?.assignees.map((assigneeId) => { - const assignee = workflowsAssignees.find((user) => user.id === assigneeId); - return assignee?.name || "Unknown Assignee"; - }); + return ( +
+ {assignees?.map((assigneeName: string, index: number) => ( + + {assigneeName} + + ))} +
+ ); + }, + }), + columnHelper.accessor("steps", { + id: "currentStep", + header: "CURRENT STEP", + cell: (info) => { + const steps = info.row.original.steps; + const currentStep = steps.find((step) => !step.completed); + const rejected = steps.find((step) => step.rejected); - return ( -
- {assignees?.map((assigneeName: string, index: number) => ( - - {assigneeName} - - ))} -
- ); - }, - }), - columnHelper.accessor("steps", { - id: "currentStep", - header: "CURRENT STEP", - cell: (info) => { - const steps = info.row.original.steps; - const currentStep = steps.find((step) => !step.completed); - const rejected = steps.find((step) => step.rejected); + return ( + + {currentStep && !rejected ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}` : "Completed"} + + ); + }, + }), + columnHelper.accessor("steps", { + header: "ACTIONS", + id: "actions", + cell: ({row}) => { + const steps = row.original.steps; + const currentStep = steps.find((step) => !step.completed); + const rejected = steps.find((step) => step.rejected); - return ( - - {currentStep && !rejected - ? `Step ${currentStep.stepNumber}: ${StepTypeLabel[currentStep.stepType]}` - : "Completed"} - - ); - }, - }), - columnHelper.accessor("steps", { - header: "ACTIONS", - id: "actions", - cell: ({ row }) => { - const steps = row.original.steps; - const currentStep = steps.find((step) => !step.completed); - const rejected = steps.find((step) => step.rejected); + return ( +
+ - return ( -
- + {currentStep && !rejected && ( + e.stopPropagation()} + data-tip="Edit" + href={`/approval-workflows/${row.original._id?.toString()}/edit`} + className="cursor-pointer tooltip"> + + + )} +
+ ); + }, + }), + ]; - {currentStep && !rejected && ( - e.stopPropagation()} - data-tip="Edit" - href={`/approval-workflows/${row.original._id?.toString()}/edit`} - className="cursor-pointer tooltip" - > - - - )} -
- ); - }, - }) - ]; + const table = useReactTable({ + data: filteredWorkflows, + columns: columns, + getCoreRowModel: getCoreRowModel(), + }); - const table = useReactTable({ - data: filteredWorkflows, - columns: columns, - getCoreRowModel: getCoreRowModel(), - }); + return ( + <> + + Approval Workflows Panel | EnCoach + + + + + +

Approval Workflows

- return ( - <> - - Approval Workflows Panel | EnCoach - - - - - -

Approval Workflows

+
+ + + +
-
- - - -
+
+
+ + +
+
+ + x.value === entityFilter)} + onChange={(value) => setEntityFilter((value?.value as CustomEntity) ?? undefined)} + isClearable + placeholder="Filter by entity..." + /> +
+
-
-
- - -
-
- - x.value === entityFilter)} - onChange={(value) => setEntityFilter((value?.value as CustomEntity) ?? undefined)} - isClearable - placeholder="Filter by entity..." - /> -
-
+ - +
+ + + {table.getHeaderGroups().map((headerGroup) => ( + + {headerGroup.headers.map((header) => ( + + ))} + + ))} + + + {table.getRowModel().rows.map((row) => ( + (window.location.href = `/approval-workflows/${row.original._id?.toString()}`)} + style={{cursor: "pointer"}} + className="bg-purple-50"> + {row.getVisibleCells().map((cell, cellIndex) => { + const lastCellIndex = row.getVisibleCells().length - 1; -
-
+ {header.isPlaceholder ? null : flexRender(header.column.columnDef.header, header.getContext())} +
- - {table.getHeaderGroups().map((headerGroup) => ( - - {headerGroup.headers.map((header) => ( - - ))} - - ))} - - - {table.getRowModel().rows.map((row) => ( - window.location.href = `/approval-workflows/${row.original._id?.toString()}`} - style={{ cursor: "pointer" }} - className="bg-purple-50" - > - {row.getVisibleCells().map((cell, cellIndex) => { - const lastCellIndex = row.getVisibleCells().length - 1; + let cellClasses = "pl-3 pr-4 py-2 border-y-2 border-mti-purple-light border-opacity-60"; + if (cellIndex === 0) { + cellClasses += " border-l-2 rounded-l-2xl"; + } + if (cellIndex === lastCellIndex) { + cellClasses += " border-r-2 rounded-r-2xl"; + } - let cellClasses = "pl-3 pr-4 py-2 border-y-2 border-mti-purple-light border-opacity-60"; - if (cellIndex === 0) { - cellClasses += " border-l-2 rounded-l-2xl"; - } - if (cellIndex === lastCellIndex) { - cellClasses += " border-r-2 rounded-r-2xl"; - } - - return ( - - ); - })} - - ))} - - -
- {header.isPlaceholder - ? null - : flexRender( - header.column.columnDef.header, - header.getContext() - )} -
- {flexRender(cell.column.columnDef.cell, cell.getContext())} -
-
- - ); + return ( + + {flexRender(cell.column.columnDef.cell, cell.getContext())} + + ); + })} + + ))} + + + + + ); } diff --git a/src/pages/entities/[id]/roles/[role].tsx b/src/pages/entities/[id]/roles/[role].tsx index 693b4370..2c90f9b9 100644 --- a/src/pages/entities/[id]/roles/[role].tsx +++ b/src/pages/entities/[id]/roles/[role].tsx @@ -107,6 +107,13 @@ const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [ {label: "Archive Assignments", key: "archive_assignment"}, ]; +const WORKFLOW_MANAGEMENT: PermissionLayout[] = [ + {label: "View Workflows", key: "view_workflows"}, + {label: "Create Workflow", key: "create_workflow"}, + {label: "Edit Workflow", key: "edit_workflow"}, + {label: "Delete Workflow", key: "delete_workflow"}, +]; + export const getServerSideProps = withIronSessionSsr(async ({req, res, params}) => { const user = await requestUser(req, res); if (!user) return redirect("/login"); @@ -399,6 +406,30 @@ export default function EntityRole({user, entity, role, userCount, disableEdit}: ))} + +
+
+ Workflow Management + permissions.includes(k))} + onChange={() => toggleMultiplePermissions(mapBy(WORKFLOW_MANAGEMENT, "key").filter(enableCheckbox))}> + Select all + +
+ +
+ {WORKFLOW_MANAGEMENT.map(({label, key}) => ( + togglePermissions(key)}> + {label} + + ))} +
+
diff --git a/src/resources/entityPermissions.ts b/src/resources/entityPermissions.ts index cb4212cc..b46ed0d0 100644 --- a/src/resources/entityPermissions.ts +++ b/src/resources/entityPermissions.ts @@ -67,7 +67,11 @@ export type RolePermission = | "pay_entity" | "view_payment_record" | "view_approval_workflows" - | "update_exam_privacy"; + | "update_exam_privacy" + | "view_workflows" + | "create_workflow" + | "edit_workflow" + | "delete_workflow"; export const DEFAULT_PERMISSIONS: RolePermission[] = [ "view_students", @@ -149,4 +153,8 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [ "pay_entity", "view_payment_record", "update_exam_privacy", + "create_workflow", + "view_workflows", + "edit_workflow", + "delete_workflow", ]; From bf2aa29b983a38377cd3682880bd644f4e59c407 Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Thu, 6 Feb 2025 23:26:21 +0000 Subject: [PATCH 2/3] implement workflow permissions --- src/pages/api/approval-workflows/[id]/edit.ts | 2 +- .../api/approval-workflows/[id]/index.ts | 22 ++- src/pages/api/approval-workflows/create.ts | 2 +- src/pages/api/approval-workflows/index.ts | 4 +- src/pages/approval-workflows/[id]/edit.tsx | 11 +- src/pages/approval-workflows/create.tsx | 13 +- src/pages/approval-workflows/index.tsx | 139 ++++++++++-------- src/pages/entities/[id]/roles/[role].tsx | 2 +- src/resources/entityPermissions.ts | 5 +- 9 files changed, 117 insertions(+), 83 deletions(-) diff --git a/src/pages/api/approval-workflows/[id]/edit.ts b/src/pages/api/approval-workflows/[id]/edit.ts index a547f0cf..e675addb 100644 --- a/src/pages/api/approval-workflows/[id]/edit.ts +++ b/src/pages/api/approval-workflows/[id]/edit.ts @@ -17,7 +17,7 @@ async function put(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } diff --git a/src/pages/api/approval-workflows/[id]/index.ts b/src/pages/api/approval-workflows/[id]/index.ts index c50026c0..d6832587 100644 --- a/src/pages/api/approval-workflows/[id]/index.ts +++ b/src/pages/api/approval-workflows/[id]/index.ts @@ -3,6 +3,8 @@ import { ApprovalWorkflow } from "@/interfaces/approval.workflow"; import { sessionOptions } from "@/lib/session"; import { requestUser } from "@/utils/api"; import { deleteApprovalWorkflow, getApprovalWorkflow, updateApprovalWorkflow } from "@/utils/approval.workflows.be"; +import { getEntityWithRoles } from "@/utils/entities.be"; +import { doesEntityAllow } from "@/utils/permissions"; import { withIronSessionApiRoute } from "iron-session/next"; import { ObjectId } from "mongodb"; import type { NextApiRequest, NextApiResponse } from "next"; @@ -19,20 +21,30 @@ async function del(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } - const { id } = req.query as { id?: string }; + const { id } = req.query as { id: string }; + const workflow = await getApprovalWorkflow("active-workflows", id); - if (id) return res.status(200).json(await deleteApprovalWorkflow("active-workflows", id)); + if (!workflow) return res.status(404).json({ ok: false }); + + const entity = await getEntityWithRoles(workflow.entityId); + if (!entity) return res.status(404).json({ ok: false }); + + if (!doesEntityAllow(user, entity, "delete_workflow") && !["admin", "developer"].includes(user.type)) { + return res.status(403).json({ ok: false }); + } + + return res.status(200).json(await deleteApprovalWorkflow("active-workflows", id)); } async function put(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } @@ -50,7 +62,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } diff --git a/src/pages/api/approval-workflows/create.ts b/src/pages/api/approval-workflows/create.ts index 96889899..aa88f9348 100644 --- a/src/pages/api/approval-workflows/create.ts +++ b/src/pages/api/approval-workflows/create.ts @@ -22,7 +22,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } diff --git a/src/pages/api/approval-workflows/index.ts b/src/pages/api/approval-workflows/index.ts index 26636186..874f0ab0 100644 --- a/src/pages/api/approval-workflows/index.ts +++ b/src/pages/api/approval-workflows/index.ts @@ -25,7 +25,7 @@ async function get(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } @@ -36,7 +36,7 @@ async function post(req: NextApiRequest, res: NextApiResponse) { const user = await requestUser(req, res); if (!user) return res.status(401).json({ ok: false }); - if (!["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) { + if (!["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) { return res.status(403).json({ ok: false }); } diff --git a/src/pages/approval-workflows/[id]/edit.tsx b/src/pages/approval-workflows/[id]/edit.tsx index 7a026bff..aec4fd1f 100644 --- a/src/pages/approval-workflows/[id]/edit.tsx +++ b/src/pages/approval-workflows/[id]/edit.tsx @@ -6,10 +6,12 @@ import Layout from "@/components/High/Layout"; import { ApprovalWorkflow, EditableApprovalWorkflow, EditableWorkflowStep, getUserTypeLabelShort } from "@/interfaces/approval.workflow"; import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { redirect, serialize } from "@/utils"; +import { findBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { getApprovalWorkflow } from "@/utils/approval.workflows.be"; +import { getEntityWithRoles } from "@/utils/entities.be"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { doesEntityAllow } from "@/utils/permissions"; import { getEntityUsers } from "@/utils/users.be"; import axios from "axios"; import { LayoutGroup, motion } from "framer-motion"; @@ -30,9 +32,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } const { id } = params as { id: string }; const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id); + if (!workflow) return redirect("/approval-workflows"); - if (!workflow) - return redirect("/approval-workflows") + const entityWithRole = await getEntityWithRoles(workflow.entityId); + if (!entityWithRole) return redirect("/approval-workflows"); + + if (!doesEntityAllow(user, entityWithRole, "edit_workflow")) return redirect("/approval-workflows"); return { props: serialize({ diff --git a/src/pages/approval-workflows/create.tsx b/src/pages/approval-workflows/create.tsx index b61d6e24..41a9d7dd 100644 --- a/src/pages/approval-workflows/create.tsx +++ b/src/pages/approval-workflows/create.tsx @@ -1,6 +1,5 @@ import Tip from "@/components/ApprovalWorkflows/Tip"; import WorkflowForm from "@/components/ApprovalWorkflows/WorkflowForm"; -import Layout from "@/components/High/Layout"; import Button from "@/components/Low/Button"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; @@ -8,11 +7,13 @@ import { ApprovalWorkflow, EditableApprovalWorkflow } from "@/interfaces/approva import { Entity } from "@/interfaces/entity"; import { CorporateUser, DeveloperUser, MasterCorporateUser, TeacherUser, User } from "@/interfaces/user"; import { sessionOptions } from "@/lib/session"; -import { redirect, serialize } from "@/utils"; +import { mapBy, redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { getApprovalWorkflowsByEntities } from "@/utils/approval.workflows.be"; -import { getEntities } from "@/utils/entities.be"; +import { getEntitiesWithRoles } from "@/utils/entities.be"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { findAllowedEntities } from "@/utils/permissions"; +import { isAdmin } from "@/utils/users"; import { getEntitiesUsers } from "@/utils/users.be"; import axios from "axios"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; @@ -31,10 +32,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { const user = await requestUser(req, res) if (!user) return redirect("/login") - if (shouldRedirectHome(user) || !["admin", "developer", "corporate", "mastercorporate"].includes(user.type)) + if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/") - const userEntitiesWithLabel = await getEntities(user.entities.map(entity => entity.id)); + const entityIDS = mapBy(user.entities, "id"); + const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); + const userEntitiesWithLabel = findAllowedEntities(user, entities, "configure_workflows"); const allConfiguredWorkflows = await getApprovalWorkflowsByEntities("configured-workflows", userEntitiesWithLabel.map(entity => entity.id)); diff --git a/src/pages/approval-workflows/index.tsx b/src/pages/approval-workflows/index.tsx index 84b65c1c..a33d2967 100644 --- a/src/pages/approval-workflows/index.tsx +++ b/src/pages/approval-workflows/index.tsx @@ -4,57 +4,34 @@ import Button from "@/components/Low/Button"; import Input from "@/components/Low/Input"; import Select from "@/components/Low/Select"; import useApprovalWorkflows from "@/hooks/useApprovalWorkflows"; -import {useAllowedEntities, useAllowedEntitiesSomePermissions, useEntityPermission} from "@/hooks/useEntityPermissions"; -import {Module, ModuleTypeLabels} from "@/interfaces"; -import {ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel} from "@/interfaces/approval.workflow"; -import {Entity, EntityWithRoles} from "@/interfaces/entity"; -import {User} from "@/interfaces/user"; -import {sessionOptions} from "@/lib/session"; -import {redirect, serialize} from "@/utils"; -import {requestUser} from "@/utils/api"; -import {getApprovalWorkflows} from "@/utils/approval.workflows.be"; -import {getEntities, getEntitiesWithRoles} from "@/utils/entities.be"; -import {shouldRedirectHome} from "@/utils/navigation.disabled"; -import {findAllowedEntities} from "@/utils/permissions"; -import {getSpecificUsers} from "@/utils/users.be"; -import {createColumnHelper, flexRender, getCoreRowModel, useReactTable} from "@tanstack/react-table"; +import { useAllowedEntities, useAllowedEntitiesSomePermissions, useEntityPermission } from "@/hooks/useEntityPermissions"; +import { Module, ModuleTypeLabels } from "@/interfaces"; +import { ApprovalWorkflow, ApprovalWorkflowStatus, ApprovalWorkflowStatusLabel, StepTypeLabel } from "@/interfaces/approval.workflow"; +import { Entity, EntityWithRoles } from "@/interfaces/entity"; +import { User } from "@/interfaces/user"; +import { sessionOptions } from "@/lib/session"; +import { mapBy, redirect, serialize } from "@/utils"; +import { requestUser } from "@/utils/api"; +import { getApprovalWorkflows } from "@/utils/approval.workflows.be"; +import { getEntities, getEntitiesWithRoles } from "@/utils/entities.be"; +import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { doesEntityAllow, findAllowedEntities } from "@/utils/permissions"; +import { isAdmin } from "@/utils/users"; +import { getSpecificUsers } from "@/utils/users.be"; +import { createColumnHelper, flexRender, getCoreRowModel, useReactTable } from "@tanstack/react-table"; import axios from "axios"; import clsx from "clsx"; -import {withIronSessionSsr} from "iron-session/next"; +import { withIronSessionSsr } from "iron-session/next"; import Head from "next/head"; import Link from "next/link"; -import {useEffect, useState} from "react"; -import {BsTrash} from "react-icons/bs"; -import {FaRegEdit} from "react-icons/fa"; -import {IoIosAddCircleOutline} from "react-icons/io"; -import {toast, ToastContainer} from "react-toastify"; +import { useRouter } from "next/router"; +import { useEffect, useState } from "react"; +import { BsTrash } from "react-icons/bs"; +import { FaRegEdit } from "react-icons/fa"; +import { IoIosAddCircleOutline } from "react-icons/io"; +import { toast, ToastContainer } from "react-toastify"; -const columnHelper = createColumnHelper(); - -export const getServerSideProps = withIronSessionSsr(async ({req, res}) => { - const user = await requestUser(req, res); - if (!user) return redirect("/login"); - - if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/"); - - const workflows = await getApprovalWorkflows("active-workflows"); - - const allAssigneeIds: string[] = [...new Set(workflows.map((workflow) => workflow.steps.map((step) => step.assignees).flat()).flat())]; - - const entities = await getEntitiesWithRoles(user.entities.map((entity) => entity.id)); - const allowedEntities = findAllowedEntities(user, entities, "view_workflows"); - - return { - props: serialize({ - user, - initialWorkflows: workflows, - workflowsAssignees: await getSpecificUsers(allAssigneeIds), - userEntitiesWithLabel: entities, - }), - }; -}, sessionOptions); - -const StatusClassNames: {[key in ApprovalWorkflowStatus]: string} = { +const StatusClassNames: { [key in ApprovalWorkflowStatus]: string } = { approved: "bg-green-100 text-green-800 border border-green-300 before:content-[''] before:w-2 before:h-2 before:bg-green-500 before:rounded-full before:inline-block before:mr-2", pending: @@ -84,6 +61,40 @@ const STATUS_OPTIONS = [ }, ]; +const columnHelper = createColumnHelper(); + +export const getServerSideProps = withIronSessionSsr(async ({ req, res }) => { + const user = await requestUser(req, res); + if (!user) return redirect("/login"); + + if (shouldRedirectHome(user) || !["admin", "developer", "teacher", "corporate", "mastercorporate"].includes(user.type)) return redirect("/"); + + const workflows = await getApprovalWorkflows("active-workflows"); + + const allAssigneeIds: string[] = [ + ...new Set( + workflows + .map(workflow => workflow.steps + .map(step => step.assignees) + .flat() + ).flat() + ) + ]; + + const entityIDS = mapBy(user.entities, "id"); + const entities = await getEntitiesWithRoles(isAdmin(user) ? undefined : entityIDS); + const allowedEntities = findAllowedEntities(user, entities, "view_workflows"); + + return { + props: serialize({ + user, + initialWorkflows: workflows, + workflowsAssignees: await getSpecificUsers(allAssigneeIds), + userEntitiesWithLabel: allowedEntities, + }), + }; +}, sessionOptions); + interface Props { user: User; initialWorkflows: ApprovalWorkflow[]; @@ -91,8 +102,8 @@ interface Props { userEntitiesWithLabel: EntityWithRoles[]; } -export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssignees, userEntitiesWithLabel}: Props) { - const {workflows, reload} = useApprovalWorkflows(); +export default function ApprovalWorkflows({ user, initialWorkflows, workflowsAssignees, userEntitiesWithLabel }: Props) { + const { workflows, reload } = useApprovalWorkflows(); const currentWorkflows = workflows || initialWorkflows; const [filteredWorkflows, setFilteredWorkflows] = useState([]); @@ -101,8 +112,10 @@ export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssi const [entityFilter, setEntityFilter] = useState(undefined); const [nameFilter, setNameFilter] = useState(""); - const allowedEntities = useAllowedEntities(user, userEntitiesWithLabel, "view_workflows"); - const allowedSomeEntities = useAllowedEntitiesSomePermissions(user, userEntitiesWithLabel, ["view_workflows", "create_workflow"]); + const router = useRouter(); + + /* const allowedEntities = useAllowedEntities(user, userEntitiesWithLabel, "view_workflows"); + const allowedSomeEntities = useAllowedEntitiesSomePermissions(user, userEntitiesWithLabel, ["view_workflows", "create_workflow"]); */ const ENTITY_OPTIONS = [ ...userEntitiesWithLabel @@ -157,10 +170,8 @@ export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssi reload(); }) .catch((reason) => { - if (reason.response.status === 404) { - toast.error("Approval Workflow not found!"); - } else if (reason.response.status === 403) { - toast.error("You do not have permission to delete an Approval Workflow!"); + if (reason.response.status === 403) { + toast.error("You do not have permission to delete this Approval Workflow!"); } else { toast.error("Something went wrong, please try again later."); } @@ -249,7 +260,7 @@ export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssi columnHelper.accessor("steps", { header: "ACTIONS", id: "actions", - cell: ({row}) => { + cell: ({ row }) => { const steps = row.original.steps; const currentStep = steps.find((step) => !step.completed); const rejected = steps.find((step) => step.rejected); @@ -259,6 +270,7 @@ export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssi {currentStep && !rejected && ( - e.stopPropagation()} + )} ); @@ -340,7 +355,7 @@ export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssi
- +
{table.getHeaderGroups().map((headerGroup) => ( @@ -357,7 +372,7 @@ export default function ApprovalWorkflows({user, initialWorkflows, workflowsAssi (window.location.href = `/approval-workflows/${row.original._id?.toString()}`)} - style={{cursor: "pointer"}} + style={{ cursor: "pointer" }} className="bg-purple-50"> {row.getVisibleCells().map((cell, cellIndex) => { const lastCellIndex = row.getVisibleCells().length - 1; diff --git a/src/pages/entities/[id]/roles/[role].tsx b/src/pages/entities/[id]/roles/[role].tsx index 2c90f9b9..2d344e18 100644 --- a/src/pages/entities/[id]/roles/[role].tsx +++ b/src/pages/entities/[id]/roles/[role].tsx @@ -109,7 +109,7 @@ const ASSIGNMENT_MANAGEMENT: PermissionLayout[] = [ const WORKFLOW_MANAGEMENT: PermissionLayout[] = [ {label: "View Workflows", key: "view_workflows"}, - {label: "Create Workflow", key: "create_workflow"}, + {label: "Configure Workflows", key: "configure_workflows"}, {label: "Edit Workflow", key: "edit_workflow"}, {label: "Delete Workflow", key: "delete_workflow"}, ]; diff --git a/src/resources/entityPermissions.ts b/src/resources/entityPermissions.ts index b46ed0d0..5691967a 100644 --- a/src/resources/entityPermissions.ts +++ b/src/resources/entityPermissions.ts @@ -69,7 +69,7 @@ export type RolePermission = | "view_approval_workflows" | "update_exam_privacy" | "view_workflows" - | "create_workflow" + | "configure_workflows" | "edit_workflow" | "delete_workflow"; @@ -81,7 +81,6 @@ export const DEFAULT_PERMISSIONS: RolePermission[] = [ "view_entity_roles", "view_statistics", "download_statistics_report", - "view_approval_workflows", ]; export const ADMIN_PERMISSIONS: RolePermission[] = [ @@ -153,7 +152,7 @@ export const ADMIN_PERMISSIONS: RolePermission[] = [ "pay_entity", "view_payment_record", "update_exam_privacy", - "create_workflow", + "configure_workflows", "view_workflows", "edit_workflow", "delete_workflow", From 00d2a7c2ad305bf2ac5aba60939a6890c693705b Mon Sep 17 00:00:00 2001 From: Joao Correia Date: Fri, 7 Feb 2025 12:57:26 +0000 Subject: [PATCH 3/3] forgot permissions on [id] view --- src/pages/approval-workflows/[id]/index.tsx | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pages/approval-workflows/[id]/index.tsx b/src/pages/approval-workflows/[id]/index.tsx index b9087217..cd051cbe 100644 --- a/src/pages/approval-workflows/[id]/index.tsx +++ b/src/pages/approval-workflows/[id]/index.tsx @@ -14,8 +14,10 @@ import useExamStore from "@/stores/exam"; import { redirect, serialize } from "@/utils"; import { requestUser } from "@/utils/api"; import { getApprovalWorkflow } from "@/utils/approval.workflows.be"; +import { getEntityWithRoles } from "@/utils/entities.be"; import { getExamById } from "@/utils/exams"; import { shouldRedirectHome } from "@/utils/navigation.disabled"; +import { doesEntityAllow } from "@/utils/permissions"; import { getSpecificUsers, getUser } from "@/utils/users.be"; import axios from "axios"; import { AnimatePresence, LayoutGroup, motion } from "framer-motion"; @@ -46,8 +48,12 @@ export const getServerSideProps = withIronSessionSsr(async ({ req, res, params } const workflow: ApprovalWorkflow | null = await getApprovalWorkflow("active-workflows", id); - if (!workflow) - return redirect("/approval-workflows") + if (!workflow) return redirect("/approval-workflows") + + const entityWithRole = await getEntityWithRoles(workflow.entityId); + if (!entityWithRole) return redirect("/approval-workflows"); + + if (!doesEntityAllow(user, entityWithRole, "view_workflows")) return redirect("/approval-workflows"); const allAssigneeIds: string[] = [ ...new Set(