From 66caff1f5e4b3fd9885f7b50daa7434810367571 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 25 Nov 2025 17:51:20 +0100 Subject: [PATCH 01/43] installed neverthrow --- src/management-system-v2/package.json | 3 ++- yarn.lock | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/package.json b/src/management-system-v2/package.json index eaf81dd7e..4ac22101c 100644 --- a/src/management-system-v2/package.json +++ b/src/management-system-v2/package.json @@ -72,7 +72,8 @@ "react-resizable": "^3.0.5", "mqtt": "^5.10.1", "bcryptjs": "3.0.2", - "sharp": "0.34.3" + "sharp": "0.34.3", + "neverthrow": "^8.2.0" }, "devDependencies": { "@tanstack/eslint-plugin-query": "5.28.11", diff --git a/yarn.lock b/yarn.lock index 7ebf9ba03..43391efc6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2693,6 +2693,11 @@ resolved "https://registry.npmjs.org/@react-email/text/-/text-0.0.7.tgz" integrity sha512-eHCx0mdllGcgK9X7wiLKjNZCBRfxRVNjD3NNYRmOc3Icbl8M9JHriJIfxBuGCmGg2UAORK5P3KmaLQ8b99/pbA== +"@rollup/rollup-linux-x64-gnu@^4.24.0": + version "4.53.3" + resolved "https://registry.yarnpkg.com/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.53.3.tgz#fd0dea3bb9aa07e7083579f25e1c2285a46cb9fa" + integrity sha512-3EhFi1FU6YL8HTUJZ51imGJWEX//ajQPfqWLI3BQq4TlvHy4X0MOr5q3D2Zof/ka0d5FNdPwZXm3Yyib/UEd+w== + "@rushstack/eslint-patch@^1.3.3": version "1.10.2" resolved "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.2.tgz" @@ -10925,6 +10930,13 @@ neo-bpmn-engine@^8.2.5: rxjs "^6.5.1" uuid "^3.3.2" +neverthrow@^8.2.0: + version "8.2.0" + resolved "https://registry.yarnpkg.com/neverthrow/-/neverthrow-8.2.0.tgz#925d988295758534d01fb7468f998680b62064f2" + integrity sha512-kOCT/1MCPAxY5iUV3wytNFUMUolzuwd/VF/1KCx7kf6CutrOsTie+84zTGTpgQycjvfLdBBdvBvFLqFD2c0wkQ== + optionalDependencies: + "@rollup/rollup-linux-x64-gnu" "^4.24.0" + next-auth@5.0.0-beta.25: version "5.0.0-beta.25" resolved "https://registry.npmjs.org/next-auth/-/next-auth-5.0.0-beta.25.tgz" From d7c1d8a0a6f35dd4ddc85ac72e34c7af52253b29 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Mon, 8 Dec 2025 14:52:58 +0100 Subject: [PATCH 02/43] # This is a combination of 2 commits. # This is the 1st commit message: temp-1 # This is the commit message #2: temp-2 errors --- .../executions/deployment-hook.ts | 2 +- .../executions/deployments-modal.tsx | 2 +- .../executions/deployments-view.tsx | 2 +- .../(automation)/executions/page.tsx | 2 +- .../iam/roles/[roleId]/rolePermissions.tsx | 2 +- .../iam/users/invite-users.tsx | 2 +- .../[mode]/[processId]/modeler-toolbar.tsx | 2 +- .../[mode]/[processId]/script-editor.tsx | 2 +- .../[environmentId]/profile/user-profile.tsx | 2 +- .../settings/@generalSettings/page.tsx | 3 +- .../app/admin/ms-config/page.tsx | 2 +- .../app/admin/spaces/page.tsx | 2 +- .../app/admin/systemadmins/page.tsx | 2 +- .../app/admin/users/page.tsx | 2 +- .../app/create-organization/page.tsx | 2 +- src/management-system-v2/app/error.tsx | 14 +- .../app/transfer-processes/server-actions.ts | 2 +- src/management-system-v2/components/auth.tsx | 34 +- .../bpmn-timeline/GanttSettingsModal.tsx | 2 +- .../components/bpmn-timeline/index.tsx | 2 +- .../components/process-modal.tsx | 2 +- .../components/share-modal/export.tsx | 6 +- .../components/share-modal/share-helpers.ts | 2 +- .../lib/data/db/engines.ts | 77 ++-- .../lib/data/db/folders.ts | 140 +++--- .../lib/data/db/html-forms.ts | 42 +- .../lib/data/db/iam/environments.ts | 157 ++++--- .../lib/data/db/iam/memberships.ts | 96 ++-- .../lib/data/db/iam/role-mappings.ts | 89 ++-- .../lib/data/db/iam/roles.ts | 69 +-- .../lib/data/db/iam/system-admins.ts | 28 +- .../lib/data/db/iam/users.ts | 209 +++++---- .../lib/data/db/iam/verification-tokens.ts | 47 +- .../lib/data/db/process.ts | 432 +++++++++++------- .../lib/data/db/space-settings.ts | 15 +- .../lib/data/db/user-tasks.ts | 69 +-- src/management-system-v2/lib/data/db/util.ts | 30 ++ src/management-system-v2/lib/data/engines.ts | 4 +- .../lib/data/environment-memberships.ts | 2 +- .../lib/data/environments.ts | 12 +- .../lib/data/file-manager-facade.ts | 2 +- src/management-system-v2/lib/data/folders.ts | 2 +- .../lib/data/html-forms.ts | 2 +- .../lib/data/processes.tsx | 2 +- .../lib/data/role-mappings.ts | 2 +- src/management-system-v2/lib/data/roles.ts | 2 +- .../lib/data/space-settings.ts | 2 +- .../lib/data/user-tasks.ts | 2 +- src/management-system-v2/lib/data/users.tsx | 2 +- .../server-actions.ts | 2 +- .../lib/engines/deployment.ts | 2 +- .../lib/engines/server-actions.ts | 2 +- src/management-system-v2/lib/errors.ts | 8 - .../lib/page-error-handling.tsx | 8 + .../lib/process-export/export-preparation.ts | 2 +- src/management-system-v2/lib/result.ts | 50 ++ .../lib/server-error-handling/errors.ts | 12 + .../page-error-response.tsx | 32 ++ .../server-error-handling/retry-button.tsx | 10 + .../{ => server-error-handling}/user-error.ts | 0 .../lib/sharing/process-sharing.ts | 2 +- src/management-system-v2/lib/ui-error.ts | 15 - .../lib/useFavouriteProcesses.ts | 2 +- .../lib/wrap-server-call.ts | 7 +- 64 files changed, 1117 insertions(+), 668 deletions(-) create mode 100644 src/management-system-v2/lib/data/db/util.ts delete mode 100644 src/management-system-v2/lib/errors.ts create mode 100644 src/management-system-v2/lib/page-error-handling.tsx create mode 100644 src/management-system-v2/lib/result.ts create mode 100644 src/management-system-v2/lib/server-error-handling/errors.ts create mode 100644 src/management-system-v2/lib/server-error-handling/page-error-response.tsx create mode 100644 src/management-system-v2/lib/server-error-handling/retry-button.tsx rename src/management-system-v2/lib/{ => server-error-handling}/user-error.ts (100%) delete mode 100644 src/management-system-v2/lib/ui-error.ts diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts index bd2a8f6bb..4ade674a7 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployment-hook.ts @@ -15,7 +15,7 @@ import { Engine } from '@/lib/engines/machines'; import { getStartFormFromMachine } from '@/lib/engines/tasklist'; import useEngines from '@/lib/engines/use-engines'; import { asyncFilter, asyncForEach, deepEquals } from '@/lib/helpers/javascriptHelpers'; -import { getErrorMessage, userError } from '@/lib/user-error'; +import { getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; import { useQuery } from '@tanstack/react-query'; import { useCallback } from 'react'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-modal.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-modal.tsx index 497508733..aad155993 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-modal.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-modal.tsx @@ -25,7 +25,7 @@ import { useEnvironment } from '@/components/auth-can'; import { getFolder, getFolderContents } from '@/lib/data/folders'; import { ProcessDeploymentList } from '@/components/process-list'; import { useQuery } from '@tanstack/react-query'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import { getAvailableSpaceEngines } from '@/lib/engines/server-actions'; import { SpaceEngine } from '@/lib/engines/machines'; import { MdOutlineComputer } from 'react-icons/md'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx index e76e55c05..5ad925a44 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/deployments-view.tsx @@ -15,7 +15,7 @@ import { useRouter } from 'next/navigation'; import { deployProcess as serverDeployProcess } from '@/lib/engines/server-actions'; import { wrapServerCall } from '@/lib/wrap-server-call'; import { SpaceEngine } from '@/lib/engines/machines'; -import { userError } from '@/lib/user-error'; +import { userError } from '@/lib/server-error-handling/user-error'; import { removeDeployment as serverRemoveDeployment } from '@/lib/engines/server-actions'; import { useQueryClient } from '@tanstack/react-query'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/page.tsx index 644e97817..35cb0244d 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/page.tsx @@ -5,7 +5,7 @@ import { getRootFolder, getFolderById, getFolderContents } from '@/lib/data/db/f import { getUsersFavourites } from '@/lib/data/users'; import { getDeployedProcessesFromSavedEngines } from '@/lib/engines/saved-engines-helpers'; import { DeployedProcessInfo } from '@/lib/engines/deployment'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import { Skeleton } from 'antd'; import { Suspense } from 'react'; import { getDbEngines } from '@/lib/data/db/engines'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx index dbbcc7372..be741e57e 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/rolePermissions.tsx @@ -8,7 +8,7 @@ import { useAbilityStore } from '@/lib/abilityStore'; import { updateRole as serverUpdateRole } from '@/lib/data/roles'; import { Role } from '@/lib/data/role-schema'; import { useEnvironment } from '@/components/auth-can'; -import { UserErrorType } from '@/lib/user-error'; +import { UserErrorType } from '@/lib/server-error-handling/user-error'; import { EnvVarsContext } from '@/components/env-vars-context'; type PermissionCategory = { diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/invite-users.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/invite-users.tsx index acff9a1ce..afb613d1b 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/invite-users.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/invite-users.tsx @@ -29,7 +29,7 @@ import { EnvVarsContext } from '@/components/env-vars-context'; import useOrganizationRoles from './use-org-roles'; import useDebounce from '@/lib/useDebounce'; import { queryUsers } from '@/lib/data/users'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import UserAvatar from '@/components/user-avatar'; import { z } from 'zod'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx index 424a8f131..c121aa6ae 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/modeler-toolbar.tsx @@ -27,7 +27,7 @@ import { ShareModal } from '@/components/share-modal/share-modal'; import { useAddControlCallback } from '@/lib/controls-store'; import { spaceURL } from '@/lib/utils'; import { generateSharedViewerUrl } from '@/lib/sharing/process-sharing'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import ScriptEditor from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-editor'; import useTimelineViewStore from '@/lib/use-timeline-view-store'; import { handleOpenDocumentation } from '../../processes-helper'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-editor.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-editor.tsx index d591493dd..4402c611f 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-editor.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-editor.tsx @@ -37,7 +37,7 @@ import { useEnvironment } from '@/components/auth-can'; import { generateScriptTaskFileName } from '@proceed/bpmn-helper'; import { type BlocklyEditorRefType } from './blockly-editor'; import { useQuery } from '@tanstack/react-query'; -import { isUserErrorResponse, userError } from '@/lib/user-error'; +import { isUserErrorResponse, userError } from '@/lib/server-error-handling/user-error'; import { wrapServerCall } from '@/lib/wrap-server-call'; import useProcessVariables from './use-process-variables'; import ProcessVariableForm from './variable-definition/process-variable-form'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx index d90bb15f9..8c1a56fc3 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/user-profile.tsx @@ -29,7 +29,7 @@ import { requestEmailChange as serverRequestEmailChange } from '@/lib/email-veri import Link from 'next/link'; import { EnvVarsContext } from '@/components/env-vars-context'; import ChangeUserPasswordModal from './change-password-modal'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; const UserProfile: FC<{ userData: User; userHasPassword: boolean }> = ({ userData, diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@generalSettings/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@generalSettings/page.tsx index f4cd38e16..36b3f93b3 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@generalSettings/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@generalSettings/page.tsx @@ -4,8 +4,7 @@ import SettingsInjector from '../settings-injector'; import { SettingGroup } from '../type-util'; import Wrapper from './wrapper'; import db from '@/lib/data/db'; -import { SpaceNotFoundError } from '@/lib/errors'; -import { getMSConfig } from '@/lib/ms-config/ms-config'; +import { SpaceNotFoundError } from '@/lib/server-error-handling/errors'; const Page = async ({ params }: { params: { environmentId: string } }) => { const { diff --git a/src/management-system-v2/app/admin/ms-config/page.tsx b/src/management-system-v2/app/admin/ms-config/page.tsx index b7173673c..d2a353a92 100644 --- a/src/management-system-v2/app/admin/ms-config/page.tsx +++ b/src/management-system-v2/app/admin/ms-config/page.tsx @@ -5,7 +5,7 @@ import { redirect } from 'next/navigation'; import { Suspense } from 'react'; import { getMSConfig, updateMSConfig, writeDefaultMSConfig } from '@/lib/ms-config/ms-config'; import MSConfigForm from './ms-config-form'; -import { userError } from '@/lib/user-error'; +import { userError } from '@/lib/server-error-handling/user-error'; import { SettingGroup } from '@/app/(dashboard)/[environmentId]/settings/type-util'; async function saveConfig(newConfig: Record) { diff --git a/src/management-system-v2/app/admin/spaces/page.tsx b/src/management-system-v2/app/admin/spaces/page.tsx index 5974ea70c..91a247bc8 100644 --- a/src/management-system-v2/app/admin/spaces/page.tsx +++ b/src/management-system-v2/app/admin/spaces/page.tsx @@ -7,7 +7,7 @@ import { import { getSystemAdminByUserId } from '@/lib/data/db/iam/system-admins'; import { redirect } from 'next/navigation'; import SpacesTable from './spaces-table'; -import { UserErrorType, userError } from '@/lib/user-error'; +import { UserErrorType, userError } from '@/lib/server-error-handling/user-error'; import Content from '@/components/content'; import { getSpaceRepresentation, getUserName } from './space-representation'; import { getUserOrganizationEnvironments } from '@/lib/data/db/iam/memberships'; diff --git a/src/management-system-v2/app/admin/systemadmins/page.tsx b/src/management-system-v2/app/admin/systemadmins/page.tsx index c780385cb..4117609bc 100644 --- a/src/management-system-v2/app/admin/systemadmins/page.tsx +++ b/src/management-system-v2/app/admin/systemadmins/page.tsx @@ -9,7 +9,7 @@ import { } from '@/lib/data/db/iam/system-admins'; import { getUserById, getUsers } from '@/lib/data/db/iam/users'; import { AuthenticatedUser } from '@/lib/data/user-schema'; -import { UserErrorType, userError } from '@/lib/user-error'; +import { UserErrorType, userError } from '@/lib/server-error-handling/user-error'; import { notFound, redirect } from 'next/navigation'; import SystemAdminsTable from './admins-table'; import { SystemAdminCreationInput } from '@/lib/data/system-admin-schema'; diff --git a/src/management-system-v2/app/admin/users/page.tsx b/src/management-system-v2/app/admin/users/page.tsx index fa3399403..8dfd020e6 100644 --- a/src/management-system-v2/app/admin/users/page.tsx +++ b/src/management-system-v2/app/admin/users/page.tsx @@ -4,7 +4,7 @@ import { getSystemAdminByUserId } from '@/lib/data/db/iam/system-admins'; import { deleteUser, getUsers } from '@/lib/data/db/iam/users'; import { notFound, redirect } from 'next/navigation'; import UserTable from './user-table'; -import { UserErrorType, userError } from '@/lib/user-error'; +import { UserErrorType, userError } from '@/lib/server-error-handling/user-error'; import Content from '@/components/content'; import { UserHasToDeleteOrganizationsError } from '@/lib/data/db/iam/users'; import { env } from '@/lib/ms-config/env-vars'; diff --git a/src/management-system-v2/app/create-organization/page.tsx b/src/management-system-v2/app/create-organization/page.tsx index 074c83f2a..5d4591523 100644 --- a/src/management-system-v2/app/create-organization/page.tsx +++ b/src/management-system-v2/app/create-organization/page.tsx @@ -3,7 +3,7 @@ import CreateOrganizationPage from './client-page'; import { getProviders } from '@/lib/auth'; import { UserOrganizationEnvironmentInput } from '@/lib/data/environment-schema'; import { addEnvironment } from '@/lib/data/db/iam/environments'; -import { getErrorMessage, userError } from '@/lib/user-error'; +import { getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; import { getMSConfig } from '@/lib/ms-config/ms-config'; import { notFound } from 'next/navigation'; import { env } from '@/lib/ms-config/env-vars'; diff --git a/src/management-system-v2/app/error.tsx b/src/management-system-v2/app/error.tsx index c504ce625..84be1eab4 100644 --- a/src/management-system-v2/app/error.tsx +++ b/src/management-system-v2/app/error.tsx @@ -3,8 +3,8 @@ import Content from '@/components/content'; import UnauthorizedFallback from '@/components/unauthorized-fallback'; import { UnauthorizedError } from '@/lib/ability/abilityHelper'; -import { SpaceNotFoundError } from '@/lib/errors'; -import { UIError } from '@/lib/ui-error'; +import { SpaceNotFoundError } from '@/lib/server-error-handling/errors'; +// import { UIError } from '@/lib/ui-error'; import { Button, Result } from 'antd'; export default function Error({ @@ -17,8 +17,8 @@ export default function Error({ let title = 'Something went wrong!'; if (error.message.startsWith(UnauthorizedError.prefix)) { return ; - } else if (error.message.startsWith(UIError.prefix)) { - title = error.message.substring(UIError.prefix.length + 2); + // } else if (error.message.startsWith(UIError.prefix)) { + // title = error.message.substring(UIError.prefix.length + 2); } const retryButton = ( @@ -36,9 +36,9 @@ export default function Error({ /> ); - if (error.message.startsWith(SpaceNotFoundError.prefix)) { - ; - } + // if (error.message.startsWith(SpaceNotFoundError.prefix)) { + // ; + // } return {feedback}; } diff --git a/src/management-system-v2/app/transfer-processes/server-actions.ts b/src/management-system-v2/app/transfer-processes/server-actions.ts index 4dd9e148c..c44ecad35 100644 --- a/src/management-system-v2/app/transfer-processes/server-actions.ts +++ b/src/management-system-v2/app/transfer-processes/server-actions.ts @@ -7,7 +7,7 @@ import { getProcesses, updateProcess } from '@/lib/data/db/process'; import { getUserById, deleteUser } from '@/lib/data/db/iam/users'; import { Process } from '@/lib/data/process-schema'; import { getGuestReference } from '@/lib/reference-guest-user-token'; -import { UserErrorType, userError } from '@/lib/user-error'; +import { UserErrorType, userError } from '@/lib/server-error-handling/user-error'; import { redirect } from 'next/navigation'; export async function transferProcesses(referenceToken: string, callbackUrl: string = '/') { diff --git a/src/management-system-v2/components/auth.tsx b/src/management-system-v2/components/auth.tsx index 4e7c073c5..f3d79eb68 100644 --- a/src/management-system-v2/components/auth.tsx +++ b/src/management-system-v2/components/auth.tsx @@ -17,15 +17,16 @@ import { cookies } from 'next/headers'; import { getMSConfig } from '@/lib/ms-config/ms-config'; import { UIError as UserUIError } from '@/lib/ui-error'; import { packedStaticRules } from '@/lib/authorization/caslRules'; +import { ok } from 'neverthrow'; export const getCurrentUser = cache(async () => { if (!env.PROCEED_PUBLIC_IAM_ACTIVE) { - return { + return ok({ session: noIamUser.session, userId: noIamUser.userId, systemAdmin: noIamUser.systemAdmin, user: noIamUser.user, - }; + }); } const session = await auth(); @@ -35,12 +36,15 @@ export const getCurrentUser = cache(async () => { userId !== '' ? getUserById(userId) : undefined, ]); + if (systemAdmin.isErr()) return systemAdmin; + if (user && user.isErr()) return user; + // Sign out user if the id doesn't correspond to a user in the db // We need to reset the cookie that stores the user id, this isn't possible // inside a server components, so we need to redirect the user to an endpoint // that logs him out, this endpoint needs to csrf protected, for this we use // the user's csrf token (which was added by next-auth) - if (userId !== '' && !user) { + if (userId !== '' && !user?.value) { const cookieStore = cookies(); const csrfToken = cookieStore.get('proceed.csrf-token')!.value; @@ -56,7 +60,7 @@ export const getCurrentUser = cache(async () => { redirect(`/api/private/signout?${searchParams}`); } - return { session, userId, systemAdmin, user }; + return ok({ session, userId, systemAdmin: systemAdmin.value, user: user?.value }); }); const systemAdminRulesForOrganizations = packedAdminRules @@ -89,7 +93,12 @@ export const getCurrentEnvironment = cache( permissionErrorHandling: { action: 'redirect' }, }, ) => { - const { userId, systemAdmin } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return currentUser; + } + + const { userId, systemAdmin } = currentUser.value; const msConfig = await getMSConfig(); if ( @@ -107,8 +116,11 @@ export const getCurrentEnvironment = cache( if (userId && !isOrganization && !env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { // Note: will be undefined for not logged in users const userOrgs = await getUserOrganizationEnvironments(userId); + if (userOrgs.isErr()) { + return userOrgs; + } - if (userOrgs.length === 0) { + if (userOrgs.value.length === 0) { if (env.PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE) { throw new UserUIError('You are not part of an organization.'); } else { @@ -116,7 +128,7 @@ export const getCurrentEnvironment = cache( } } - activeSpace = userOrgs[0]; + activeSpace = userOrgs.value[0]; isOrganization = true; } @@ -125,10 +137,10 @@ export const getCurrentEnvironment = cache( if (systemAdmin || !msConfig.PROCEED_PUBLIC_IAM_ACTIVE) { const rules = getSystemAdminRules(isOrganization); - return { + return ok({ ability: new Ability(rules, activeSpace), activeEnvironment: { spaceId: activeSpace, isOrganization }, - }; + }); } if (!userId || !isMember(decodeURIComponent(spaceIdParam), userId)) { @@ -147,9 +159,9 @@ export const getCurrentEnvironment = cache( const ability = await getAbilityForUser(userId, activeSpace); - return { + return ok({ ability, activeEnvironment: { spaceId: activeSpace, isOrganization }, - }; + }); }, ); diff --git a/src/management-system-v2/components/bpmn-timeline/GanttSettingsModal.tsx b/src/management-system-v2/components/bpmn-timeline/GanttSettingsModal.tsx index f6add6c1a..9850ca55c 100644 --- a/src/management-system-v2/components/bpmn-timeline/GanttSettingsModal.tsx +++ b/src/management-system-v2/components/bpmn-timeline/GanttSettingsModal.tsx @@ -7,7 +7,7 @@ import { useEnvironment } from '@/components/auth-can'; import { SettingsGroup } from '../../app/(dashboard)/[environmentId]/settings/components'; import { debouncedSettingsUpdate } from '../../app/(dashboard)/[environmentId]/settings/utils'; import { getSpaceSettingsValues } from '@/lib/data/space-settings'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import { ganttViewSettingsDefinition } from './gantt-settings-definition'; import { createGanttSettingsRenderer } from './gantt-settings-utils'; import type { SettingGroup } from '../../app/(dashboard)/[environmentId]/settings/type-util'; diff --git a/src/management-system-v2/components/bpmn-timeline/index.tsx b/src/management-system-v2/components/bpmn-timeline/index.tsx index 72ace0288..8b378ae3f 100644 --- a/src/management-system-v2/components/bpmn-timeline/index.tsx +++ b/src/management-system-v2/components/bpmn-timeline/index.tsx @@ -7,7 +7,7 @@ import { GanttChartCanvas } from '@/components/gantt-chart-canvas'; import type { GanttElementType, GanttDependency } from '@/components/gantt-chart-canvas/types'; import useTimelineViewStore from '@/lib/use-timeline-view-store'; import { getSpaceSettingsValues } from '@/lib/data/space-settings'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import { useEnvironment } from '@/components/auth-can'; import { moddle } from '@proceed/bpmn-helper'; import useModelerStateStore from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-modeler-state-store'; diff --git a/src/management-system-v2/components/process-modal.tsx b/src/management-system-v2/components/process-modal.tsx index b0c863a62..5f884639e 100644 --- a/src/management-system-v2/components/process-modal.tsx +++ b/src/management-system-v2/components/process-modal.tsx @@ -18,7 +18,7 @@ import { Skeleton, } from 'antd'; import { MdArrowBackIos, MdArrowForwardIos } from 'react-icons/md'; -import { UserError } from '@/lib/user-error'; +import { UserError } from '@/lib/server-error-handling/user-error'; import { useAddControlCallback } from '@/lib/controls-store'; import { checkIfProcessExistsByName } from '@/lib/data/processes'; import { useEnvironment } from './auth-can'; diff --git a/src/management-system-v2/components/share-modal/export.tsx b/src/management-system-v2/components/share-modal/export.tsx index d79626f99..43b7e3e07 100644 --- a/src/management-system-v2/components/share-modal/export.tsx +++ b/src/management-system-v2/components/share-modal/export.tsx @@ -14,7 +14,11 @@ import useModelerStateStore from '@/app/(dashboard)/[environmentId]/processes/[m import { is as bpmnIs } from 'bpmn-js/lib/util/ModelUtil'; import { ProcessMetadata } from '@/lib/data/process-schema'; import useProcessVersion from './use-process-version'; -import { UserError, UserErrorType, isUserErrorResponse } from '@/lib/user-error'; +import { + UserError, + UserErrorType, + isUserErrorResponse, +} from '@/lib/server-error-handling/user-error'; import { FaRegQuestionCircle } from 'react-icons/fa'; export type ProcessExportTypes = ProcessExportOptions['type'] | 'pdf'; diff --git a/src/management-system-v2/components/share-modal/share-helpers.ts b/src/management-system-v2/components/share-modal/share-helpers.ts index 572f29d6c..21cedb316 100644 --- a/src/management-system-v2/components/share-modal/share-helpers.ts +++ b/src/management-system-v2/components/share-modal/share-helpers.ts @@ -4,7 +4,7 @@ import { updateProcessGuestAccessRights, } from '@/lib/sharing/process-sharing'; import { wrapServerCall } from '@/lib/wrap-server-call'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; export function updateShare( { diff --git a/src/management-system-v2/lib/data/db/engines.ts b/src/management-system-v2/lib/data/db/engines.ts index 118961e74..f411eca86 100644 --- a/src/management-system-v2/lib/data/db/engines.ts +++ b/src/management-system-v2/lib/data/db/engines.ts @@ -3,6 +3,7 @@ import { toCaslResource } from '@/lib/ability/caslAbility'; import db from '@/lib/data/db'; import { SpaceEngineInput, SpaceEngineInputSchema } from '@/lib/space-engine-schema'; import { SystemAdmin } from '@prisma/client'; +import { ok, err } from 'neverthrow'; export async function getDbEngines( environmentId: string | null, @@ -11,13 +12,13 @@ export async function getDbEngines( ) { // engines without an environmentId are PROCEED engines if (environmentId === null && systemAdmin !== 'dont-check' && !systemAdmin) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); const engines = await db.engine.findMany({ where: { environmentId: environmentId }, }); - return ability ? ability.filter('view', 'Machine', engines) : engines; + return ok(ability ? ability.filter('view', 'Machine', engines) : engines); } export async function getDbEngineById( @@ -28,7 +29,7 @@ export async function getDbEngineById( ) { // engines without an environmentId are PROCEED engines if (environmentId === null && systemAdmin !== 'dont-check' && !systemAdmin) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); const engine = await db.engine.findUnique({ where: { @@ -37,16 +38,16 @@ export async function getDbEngineById( }, }); - if (!engine) return undefined; + if (!engine) return ok(undefined); if ( ability && !ability.can('view', toCaslResource('Machine', engine), { environmentId: environmentId! }) ) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - return engine; + return ok(engine); } export async function getDbEngineByAddress( @@ -57,7 +58,7 @@ export async function getDbEngineByAddress( ) { // engines without an environmentId are PROCEED engines if (spaceId === null && systemAdmin !== 'dont-check' && !systemAdmin) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); const engine = await db.engine.findFirst({ where: { @@ -67,17 +68,17 @@ export async function getDbEngineByAddress( }); if (!engine) { - if (ability && !ability.can('view', 'Machine')) throw new UnauthorizedError(); - return undefined; + if (ability && !ability.can('view', 'Machine')) return err(new UnauthorizedError()); + return ok(undefined); } if ( ability && !ability.can('view', toCaslResource('Machine', engine), { environmentId: spaceId! }) ) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); - return engine; + return ok(engine); } const SpaceEngineArraySchema = SpaceEngineInputSchema.array(); @@ -89,15 +90,17 @@ export async function addDbEngines( ) { // engines without an environmentId are PROCEED engines if (environmentId === null && systemAdmin !== 'dont-check' && !systemAdmin) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); const newEngines = SpaceEngineArraySchema.parse(enginesInput); - if (ability && !ability.can('create', 'Machine')) throw new UnauthorizedError(); + if (ability && !ability.can('create', 'Machine')) return err(new UnauthorizedError()); - return db.engine.createMany({ - data: newEngines.map((e) => ({ ...e, environmentId: environmentId ?? null })), - }); + return ok( + db.engine.createMany({ + data: newEngines.map((e) => ({ ...e, environmentId: environmentId ?? null })), + }), + ); } const PartialSpaceEngineInputSchema = SpaceEngineInputSchema.partial(); @@ -110,26 +113,28 @@ export async function updateDbEngine( ) { // engines without an environmentId are PROCEED engines if (environmentId === null && systemAdmin !== 'dont-check' && !systemAdmin) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); const newEngineData = PartialSpaceEngineInputSchema.parse(engineInput); if (ability) { const engine = await getDbEngineById(engineId, environmentId, ability, systemAdmin); - if (!engine) throw new Error('Engine not found'); + if (!engine) return err(new Error('Engine not found')); if ( !ability.can('update', toCaslResource('Machine', engine), { environmentId: environmentId! }) ) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - return await db.engine.update({ - data: newEngineData, - where: { - environmentId, - id: engineId, - }, - }); + return ok( + await db.engine.update({ + data: newEngineData, + where: { + environmentId, + id: engineId, + }, + }), + ); } export async function deleteSpaceEngine( @@ -140,21 +145,23 @@ export async function deleteSpaceEngine( ) { // engines without an environmentId are PROCEED engines if (environmentId === null && systemAdmin !== 'dont-check' && !systemAdmin) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); if (ability) { const engine = await getDbEngineById(engineId, environmentId, ability, systemAdmin); - if (!engine) throw new Error('Engine not found'); + if (!engine) return err(new Error('Engine not found')); if ( !ability.can('delete', toCaslResource('Machine', engine), { environmentId: environmentId! }) ) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - return await db.engine.delete({ - where: { - environmentId: environmentId, - id: engineId, - }, - }); + return ok( + await db.engine.delete({ + where: { + environmentId: environmentId, + id: engineId, + }, + }), + ); } diff --git a/src/management-system-v2/lib/data/db/folders.ts b/src/management-system-v2/lib/data/db/folders.ts index d4359b55b..b2cab9181 100644 --- a/src/management-system-v2/lib/data/db/folders.ts +++ b/src/management-system-v2/lib/data/db/folders.ts @@ -1,8 +1,9 @@ +import { ok, err, Result } from 'neverthrow'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { Folder, FolderInput, FolderSchema, FolderUserInput } from '../folder-schema'; import { toCaslResource } from '@/lib/ability/caslAbility'; import { v4 } from 'uuid'; -import { Process, ProcessMetadata } from '../process-schema'; +import { ProcessMetadata } from '../process-schema'; import db from '@/lib/data/db'; import { getProcess } from './process'; import { Prisma } from '@prisma/client'; @@ -16,14 +17,14 @@ export async function getRootFolder(environmentId: string, ability?: Ability) { }); if (!rootFolder) { - throw new Error(`MS Error: environment ${environmentId} has no root folder`); + return err(new Error(`MS Error: environment ${environmentId} has no root folder`)); } if (ability && !ability.can('view', toCaslResource('Folder', rootFolder))) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - return rootFolder; + return ok(rootFolder); } export async function getFolderById(folderId: string, ability?: Ability) { @@ -37,21 +38,21 @@ export async function getFolderById(folderId: string, ability?: Ability) { }); if (!folder) { - throw new Error('Folder not found'); + return err(new Error('Folder not found')); } if (ability && !ability.can('view', toCaslResource('Folder', folder))) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - return folder; + return ok(folder); } export async function getFolders(spaceId?: string) { const selection = await db.folder.findMany({ where: { environmentId: spaceId }, }); - return selection; + return ok(selection); } export async function getFolderChildren(folderId: string, ability?: Ability) { @@ -66,59 +67,64 @@ export async function getFolderChildren(folderId: string, ability?: Ability) { }); if (!folder) { - throw new Error('Folder not found'); + return err(new Error('Folder not found')); } if (ability && !ability.can('view', toCaslResource('Folder', folder))) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } const combinedResults = [ ...folder.childrenFolder.map((child) => ({ ...child, type: 'folder' })), ...folder.processes.map((process) => ({ ...process, type: process.type.toLowerCase() })), ]; - return combinedResults; + return ok(combinedResults); } export async function getFolderContents(folderId: string, ability?: Ability) { const folderChildren = await getFolderChildren(folderId, ability); + if (folderChildren.isErr()) return folderChildren; + const folderContent: ((Folder & { type: 'folder' }) | ProcessMetadata)[] = []; - for (let i = 0; i < folderChildren.length; i++) { + for (let i = 0; i < folderChildren.value.length; i++) { try { - const child = folderChildren[i]; + const child = folderChildren.value[i]; if (child.type !== 'folder') { - const process = (await getProcess(child.id)) as unknown as Process; + const process = await getProcess(child.id); + if (process.isErr()) return process; + // NOTE: this check should probably done inside inside getprocess - if (ability && !ability.can('view', toCaslResource('Process', process))) continue; - folderContent.push(process); + if (ability && !ability.can('view', toCaslResource('Process', process.value))) continue; + folderContent.push(process.value as ProcessMetadata); } else { - folderContent.push({ ...(await getFolderById(child.id, ability)), type: 'folder' }); + const folder = await getFolderById(child.id, ability); + if (folder.isErr()) return folder; + + folderContent.push({ ...folder.value, type: 'folder' }); } } catch (e) {} } - return folderContent; + return ok(folderContent); } -export async function createFolder( +// This is needed to inferr the return type +async function _createFolder( folderInput: FolderInput, - ability?: Ability, - tx?: Prisma.TransactionClient, -): Promise { - if (!tx) { - return await db.$transaction(async (trx: Prisma.TransactionClient) => { - return await createFolder(folderInput, ability, trx); - }); - } + ability: Ability | undefined, + tx: Prisma.TransactionClient, +) { + const folderParseResult = FolderSchema.safeParse(folderInput); + if (!folderParseResult.success) return err(folderParseResult.error); - const folder = FolderSchema.parse(folderInput); + const folder = folderParseResult.data; if (!folder.id) folder.id = v4(); // Checks if (ability && !ability.can('create', toCaslResource('Folder', folder))) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); const existingFolder = await db.folder.findUnique({ where: { @@ -126,7 +132,7 @@ export async function createFolder( }, }); if (existingFolder) { - throw new Error('Folder already exists'); + return err(new Error('Folder already exists')); } if (folder.parentId) { @@ -137,12 +143,13 @@ export async function createFolder( }); if (!parentFolder) { - throw new Error('Parent folder does not exist'); + return err(new Error('Parent folder does not exist')); } if (parentFolder.environmentId !== folder.environmentId) { - throw new Error('Parent folder is in a different environment'); + return err(new Error('Parent folder is in a different environment')); } + await tx.folder.update({ where: { id: folder.parentId, @@ -160,7 +167,7 @@ export async function createFolder( }); if (rootFolder) { - throw new Error(`Environment ${folder.environmentId} already has a root folder`); + return err(new Error(`Environment ${folder.environmentId} already has a root folder`)); } } @@ -176,7 +183,20 @@ export async function createFolder( }, }); - return createdFolder; + return ok(createdFolder); +} +export async function createFolder( + folderInput: FolderInput, + ability?: Ability, + tx?: Prisma.TransactionClient, +) { + if (!tx) { + return await db.$transaction(async (trx: Prisma.TransactionClient) => { + return await _createFolder(folderInput, ability, trx); + }); + } else { + return _createFolder(folderInput, ability, tx); + } } /** Deletes a folder and every child recursively */ @@ -187,18 +207,18 @@ export async function deleteFolder(folderId: string, ability?: Ability) { }); if (!folderToDelete) { - throw new Error('Folder not found'); + return err(new Error('Folder not found')); } if (ability && !ability.can('delete', toCaslResource('Folder', folderToDelete))) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } await db.folder.delete({ where: { id: folderId }, }); - return { success: true }; + return ok(); } export async function updateFolderMetaData( @@ -211,15 +231,15 @@ export async function updateFolderMetaData( }); if (!folder) { - throw new Error('Folder not found'); + return err(new Error('Folder not found')); } if (ability && !ability.can('update', toCaslResource('Folder', folder))) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } if (newMetaDataInput.environmentId && newMetaDataInput.environmentId !== folder.environmentId) { - throw new Error('environmentId cannot be changed'); + return err(new Error('environmentId cannot be changed')); } const updatedFolder = await db.folder.update({ @@ -227,17 +247,17 @@ export async function updateFolderMetaData( data: { ...newMetaDataInput, lastEditedOn: new Date() }, }); - return updatedFolder; + return ok(updatedFolder); } -async function isInSubtree(rootId: string, nodeId: string) { +async function isInSubtree(rootId: string, nodeId: string): Promise> { const folderData = await db.folder.findUnique({ where: { id: rootId }, include: { childrenFolder: true }, }); if (!folderData) { - throw new Error('RootId not found'); + return err(new Error('RootId not found')); } const nodeFolder = await db.folder.findUnique({ @@ -245,16 +265,20 @@ async function isInSubtree(rootId: string, nodeId: string) { }); if (!nodeFolder) { - throw new Error('NodeId not found'); + return err(new Error('NodeId not found')); } if (rootId === nodeId) { - return true; + return ok(true); } + for (const child of folderData.childrenFolder) { - if (await isInSubtree(child.id, nodeId)) return true; + const recursiveCallResult = await isInSubtree(child.id, nodeId); + if (recursiveCallResult.isErr()) return recursiveCallResult; + + if (recursiveCallResult.value) return ok(true); } - return false; + return ok(false); } export async function moveFolder(folderId: string, newParentId: string, ability?: Ability) { @@ -264,11 +288,11 @@ export async function moveFolder(folderId: string, newParentId: string, ability? }); if (!folder) { - throw new Error('Folder not found'); + return err(new Error('Folder not found')); } if (!folder.parentId) { - throw new Error('Root folders cannot be moved'); + return err(new Error('Root folders cannot be moved')); } if (folder.parentId === newParentId) { @@ -280,11 +304,11 @@ export async function moveFolder(folderId: string, newParentId: string, ability? }); if (!newParentFolder) { - throw new Error('New parent folder not found'); + return err(new Error('New parent folder not found')); } if (newParentFolder.environmentId !== folder.environmentId) { - throw new Error('Cannot move folder to a different environment'); + return err(new Error('Cannot move folder to a different environment')); } // Check permissions @@ -296,12 +320,12 @@ export async function moveFolder(folderId: string, newParentId: string, ability? ability.can('update', toCaslResource('Folder', folder.parentFolder!)) ) ) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } // Check if moving to its own subtree if (await isInSubtree(folderId, newParentId)) { - throw new Error('Folder cannot be moved to its children'); + return err(new Error('Folder cannot be moved to its children')); } // Update folder @@ -321,9 +345,9 @@ export async function moveProcess(processId: string, newParentId: string, abilit where: { id: processId }, }); - if (!process) throw new Error('Folder not found'); + if (!process) return err(new Error('Folder not found')); - if (process.folderId === newParentId) return; + if (process.folderId === newParentId) return ok(); const [oldParentFolder, newParentFolder] = await Promise.all([ db.folder.findUnique({ @@ -335,10 +359,10 @@ export async function moveProcess(processId: string, newParentId: string, abilit }), ]); - if (!newParentFolder) throw new Error('New parent folder not found'); + if (!newParentFolder) return err(new Error('New parent folder not found')); if (newParentFolder.environmentId !== process.environmentId) - throw new Error('Cannot move folder to a different environment'); + return err(new Error('Cannot move folder to a different environment')); // Check permissions if ( @@ -349,7 +373,7 @@ export async function moveProcess(processId: string, newParentId: string, abilit ability.can('update', toCaslResource('Folder', oldParentFolder!)) ) ) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } // Update process diff --git a/src/management-system-v2/lib/data/db/html-forms.ts b/src/management-system-v2/lib/data/db/html-forms.ts index 0039271cd..d40e935ab 100644 --- a/src/management-system-v2/lib/data/db/html-forms.ts +++ b/src/management-system-v2/lib/data/db/html-forms.ts @@ -1,7 +1,8 @@ +import { ok, err } from 'neverthrow'; import Ability from '@/lib/ability/abilityHelper'; import db from '@/lib/data/db'; import { HtmlForm, HtmlFormMetaDataSchema, HtmlFormSchema } from '@/lib/html-form-schema'; -import { UserFacingError } from '@/lib/user-error'; +import { UserFacingError } from '@/lib/server-error-handling/user-error'; /** * Returns all html forms in an environment @@ -27,8 +28,13 @@ export async function getHtmlForms(environmentId: string, ability?: Ability) { //TODO: use ability // return ability ? ability.filter('view', 'Html Form', spaceForms) : spaceForms; + const parseResult = HtmlFormMetaDataSchema.array().safeParse(spaceForms); - return HtmlFormMetaDataSchema.array().parse(spaceForms); + if (parseResult.success) { + return ok(parseResult.data); + } else { + return err(parseResult.error); + } } export async function getHtmlForm(formId: string) { @@ -53,15 +59,26 @@ export async function getHtmlForm(formId: string) { }); if (!form) { - throw new UserFacingError(`Html form with id ${formId} does not exist!`); + return err(new UserFacingError(`Html form with id ${formId} does not exist!`)); } - return HtmlFormSchema.parse(form); + const parseResult = HtmlFormSchema.safeParse(form); + + if (parseResult.success) { + return ok(parseResult.data); + } else { + return err(parseResult.error); + } } /** Handles adding a html form */ export async function addHtmlForm(formInput: HtmlForm) { - const form = HtmlFormSchema.parse(formInput); + const parseResult = HtmlFormSchema.safeParse(formInput); + if (!parseResult.success) { + return err(parseResult.error); + } + + const form = parseResult.data; // check if there is an id collision const existingForm = await db.htmlForm.findUnique({ @@ -70,7 +87,7 @@ export async function addHtmlForm(formInput: HtmlForm) { }, }); if (existingForm) { - throw new Error(`Html form with id ${formInput.id} already exists!`); + return err(new Error(`Html form with id ${formInput.id} already exists!`)); } // save form info @@ -81,7 +98,12 @@ export async function addHtmlForm(formInput: HtmlForm) { /** Updates an existing form */ export async function updateHtmlForm(formId: string, newInfoInput: Partial) { - const formInput = HtmlFormSchema.partial().parse(newInfoInput); + const parseResult = HtmlFormSchema.partial().safeParse(newInfoInput); + if (!parseResult.success) { + return err(parseResult.error); + } + + const formInput = parseResult.data; const existingForm = await db.htmlForm.findUnique({ where: { @@ -90,7 +112,7 @@ export async function updateHtmlForm(formId: string, newInfoInput: Partial { await tx.space.update({ @@ -69,7 +80,7 @@ export async function activateEnvrionment(environmentId: string, userId: string) [ { environmentId, - roleId: adminRole.id, + roleId: adminRole.value.id, userId, }, ], @@ -79,27 +90,32 @@ export async function activateEnvrionment(environmentId: string, userId: string) }); } -export async function addEnvironment( +export const addEnvironment = ensureTransactionWrapper(_addEnvironment, 2); +async function _addEnvironment( environmentInput: EnvironmentInput, ability?: Ability, - tx?: Prisma.TransactionClient, -): Promise { - // If `tx` is provided, use it; otherwise, start a new transaction - if (!tx) { - return db.$transaction(async (trx) => { - return await addEnvironment(environmentInput, ability, trx); - }); - } + _tx?: Prisma.TransactionClient, +) { + const tx = _tx!; - const dbMutator = tx; + const parseResult = environmentSchema.safeParse(environmentInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const newEnvironment = parseResult.data; - const newEnvironment = environmentSchema.parse(environmentInput); const id = newEnvironment.isOrganization ? newEnvironment.id ?? v4() : newEnvironment.ownerId; - if (await getEnvironmentById(id)) throw new Error('Environment id already exists'); + const existingEnvironment = await getEnvironmentById(id); + if (existingEnvironment.isErr()) { + return existingEnvironment; + } + if (existingEnvironment.value) { + return err(new Error('Environment id already exists')); + } const newEnvironmentWithId = { ...newEnvironment, id }; - await dbMutator.space.create({ data: { ...newEnvironmentWithId } }); + await tx.space.create({ data: { ...newEnvironmentWithId } }); if (newEnvironment.isOrganization) { const adminRole = await addRole( @@ -112,7 +128,9 @@ export async function addEnvironment( undefined, tx, ); - await addRole( + if (adminRole.isErr()) return adminRole; + + const guestRole = await addRole( { environmentId: id, name: '@guest', @@ -122,7 +140,9 @@ export async function addEnvironment( undefined, tx, ); - await addRole( + if (guestRole.isErr()) return guestRole; + + const everyoneRole = await addRole( { environmentId: id, name: '@everyone', @@ -132,26 +152,29 @@ export async function addEnvironment( undefined, tx, ); + if (everyoneRole.isErr()) return everyoneRole; if (newEnvironment.isActive) { - await addMember(id, newEnvironment.ownerId, undefined, tx); + const ownerAdded = await addMember(id, newEnvironment.ownerId, undefined, tx); + if (ownerAdded?.isErr()) return ownerAdded; - await addRoleMappings( + const adminRoleMapping = await addRoleMappings( [ { environmentId: id, - roleId: adminRole.id, + roleId: adminRole.value.id, userId: newEnvironment.ownerId, }, ], undefined, tx, ); + if (adminRoleMapping?.isErr()) return adminRoleMapping; } } // add root folder - await createFolder( + const rootFolder = await createFolder( { environmentId: id, name: '', @@ -161,21 +184,24 @@ export async function addEnvironment( undefined, tx, ); + if (rootFolder.isErr()) return rootFolder; - return newEnvironmentWithId; + return ok(newEnvironmentWithId); } export async function deleteEnvironment(environmentId: string, ability?: Ability) { const environment = await getEnvironmentById(environmentId); - if (!environment) throw new Error('Environment not found'); + if (environment.isErr() || !environment.value) return err(new Error('Environment not found')); - if (env.PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE && environment.isOrganization) { - throw new Error( - 'Organizations cannot be deleted when PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE is true', + if (env.PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE && environment.value.isOrganization) { + return err( + new Error( + 'Organizations cannot be deleted when PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE is true', + ), ); } - if (ability && !ability.can('delete', 'Environment')) throw new UnauthorizedError(); + if (ability && !ability.can('delete', 'Environment')) return err(new UnauthorizedError()); await db.space.delete({ where: { id: environmentId }, }); @@ -189,25 +215,36 @@ export async function updateOrganization( ability?: Ability, ) { const environment = await getEnvironmentById(environmentId, ability, { throwOnNotFound: true }); - - if (!environment) { - throw new Error('Environment not found'); + if (environment.isErr()) { + return environment; + } + if (!environment.value) { + return err(new Error('Environment not found')); } if ( ability && !ability.can('update', toCaslResource('Environment', environment), { environmentId }) ) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); - if (!environment.isOrganization) throw new Error('Environment is not an organization'); + if (!environment.value.isOrganization) + return err(new Error('Environment is not an organization')); - const update = UserOrganizationEnvironmentInputSchema.partial().parse(environmentInput); - const newEnvironmentData: Environment = { ...environment, ...update } as Environment; + const updateParseResult = + UserOrganizationEnvironmentInputSchema.partial().safeParse(environmentInput); + if (!updateParseResult.success) { + return err(updateParseResult.error); + } - await db.space.update({ where: { id: environment.id }, data: { ...newEnvironmentData } }); + const newEnvironmentData: Environment = { + ...environment.value, + ...updateParseResult.data, + } as Environment; - return newEnvironmentData; + await db.space.update({ where: { id: environment.value.id }, data: { ...newEnvironmentData } }); + + return ok(newEnvironmentData); } // TODO below: implement db logic @@ -216,27 +253,32 @@ export async function saveSpaceLogo(organizationId: string, image: Buffer, abili const organization = await getEnvironmentById(organizationId, undefined, { throwOnNotFound: true, }); - if (!organization?.isOrganization) - throw new Error("You can't save a logo for a personal environment"); + if (organization.isErr()) { + return organization; + } + if (!organization.value?.isOrganization) + return err(new Error("You can't save a logo for a personal environment")); if (ability && ability.can('update', 'Environment', { environmentId: organizationId })) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); try { //saveLogo(organizationId, image); - } catch (err) { - throw new Error('Failed to store image'); + } catch (error) { + return err(new Error('Failed to store image')); } } export async function getSpaceLogo(organizationId: string) { try { - return await db.space.findUnique({ - where: { id: organizationId }, - select: { spaceLogo: true }, - }); - } catch (err) { - return undefined; + return ok( + await db.space.findUnique({ + where: { id: organizationId }, + select: { spaceLogo: true }, + }), + ); + } catch (error) { + return err(error); } } @@ -246,9 +288,10 @@ export async function spaceHasLogo(organizationId: string) { select: { spaceLogo: true }, }); if (res?.spaceLogo) { - return true; + return ok(true); } - return false; + + return ok(false); } export async function deleteSpaceLogo(organizationId: string) { diff --git a/src/management-system-v2/lib/data/db/iam/memberships.ts b/src/management-system-v2/lib/data/db/iam/memberships.ts index 44ce5167c..7ccc17a1f 100644 --- a/src/management-system-v2/lib/data/db/iam/memberships.ts +++ b/src/management-system-v2/lib/data/db/iam/memberships.ts @@ -1,11 +1,13 @@ +import { ok, err } from 'neverthrow'; import { z } from 'zod'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { getEnvironmentById } from './environments'; import { v4 } from 'uuid'; -import { ActiveOrganizationEnvironment, Environment } from '../../environment-schema.js'; +import { ActiveOrganizationEnvironment } from '../../environment-schema.js'; import db from '@/lib/data/db'; import { Prisma } from '@prisma/client'; import { UserHasToDeleteOrganizationsError } from './users'; +import { ensureTransactionWrapper } from '../util'; const MembershipInputSchema = z.object({ userId: z.string(), @@ -19,36 +21,25 @@ export type Membership = MembershipInput & { createdOn: string; }; -function isOrganization(environment: Environment, opts: { throwIfNotFound?: boolean } = {}) { - if (!environment) - if (opts.throwIfNotFound) throw new Error('Environment not found'); - else return false; - - if (!environment.isOrganization) - if (opts.throwIfNotFound) - throw new Error("Environment isn't an organization, it can't have members"); - else return false; - - return true; -} - export async function getUserOrganizationEnvironments(userId: string) { - return ( - await db.space.findMany({ - where: { - isOrganization: true, - //ownerId: userId, - members: { - some: { - userId: userId, + return ok( + ( + await db.space.findMany({ + where: { + isOrganization: true, + //ownerId: userId, + members: { + some: { + userId: userId, + }, }, }, - }, - select: { - id: true, - }, - }) - ).map((workspace) => workspace.id); + select: { + id: true, + }, + }) + ).map((workspace) => workspace.id), + ); } export async function getMembers(environmentId: string, ability?: Ability) { @@ -64,8 +55,8 @@ export async function getMembers(environmentId: string, ability?: Ability) { members: true, }, }); - if (!workspace) throw new Error('Environment not found'); - return workspace.members; + if (!workspace) return err(new Error('Environment not found')); + return ok(workspace.members); } export async function getFullMembersWithRoles(environmentId: string, ability?: Ability) { @@ -126,7 +117,7 @@ export async function getUsersInSpace(spaceId: string, ability?: Ability) { }, }); - return users; + return ok(users); } export async function isMember( @@ -134,11 +125,14 @@ export async function isMember( userId: string, tx?: Prisma.TransactionClient, ) { - const dbMutator = tx || db; + const dbMutator = tx!; const environment = await getEnvironmentById(environmentId, undefined, undefined, tx); - if (!environment?.isOrganization) { - return userId === environmentId; + if (environment.isErr()) { + return environment; + } + if (!environment.value?.isOrganization) { + return ok(userId === environmentId); } const membership = await dbMutator.membership.findFirst({ where: { @@ -146,7 +140,7 @@ export async function isMember( userId: userId, }, }); - return membership ? true : false; + return ok(membership ? true : false); } export async function addMember( @@ -172,8 +166,8 @@ export async function addMember( where: { id: userId }, }); - if (!user) throw new Error('User not found'); - if (user.isGuest) throw new Error('Guest users cannot be added to environments'); + if (!user) return err(new Error('User not found')); + if (user.isGuest) return err(new Error('Guest users cannot be added to environments')); await dbMutator.membership.create({ data: { @@ -185,18 +179,13 @@ export async function addMember( }); } -export async function removeMember( +export const removeMember = ensureTransactionWrapper(_removeMember, 2); +async function _removeMember( environmentId: string, userId: string, ability?: Ability, _tx?: Prisma.TransactionClient, -): Promise { - if (!_tx) { - return db.$transaction(async (tx) => { - await removeMember(environmentId, userId, ability, tx); - }); - } - +) { const tx = _tx!; const environment = await tx.space.findUnique({ @@ -205,7 +194,7 @@ export async function removeMember( }); if (!environment) { - throw new Error('Environment not found'); + return err(new Error('Environment not found')); } const organization = environment as ActiveOrganizationEnvironment; @@ -213,8 +202,11 @@ export async function removeMember( if (ability) ability; const memberExists = await isMember(environmentId, userId, tx); - if (!memberExists) { - throw new Error('User is not a member of this environment'); + if (memberExists.isErr()) { + return memberExists; + } + if (!memberExists.value) { + return err(new Error('User is not a member of this environment')); } const adminRole = await tx.role.findFirst({ @@ -231,11 +223,13 @@ export async function removeMember( }, }); if (!adminRole) - throw new Error(`Consistency error: admin role of environment ${environmentId} not found`); + return err( + new Error(`Consistency error: admin role of environment ${environmentId} not found`), + ); if (adminRole.members.find((role) => role.userId === userId)) { if (adminRole.members.length === 1) { - throw new UserHasToDeleteOrganizationsError([environmentId]); + return err(new UserHasToDeleteOrganizationsError([environmentId])); } if (organization.ownerId === userId) { @@ -253,4 +247,6 @@ export async function removeMember( userId: userId, }, }); + + return ok(); } diff --git a/src/management-system-v2/lib/data/db/iam/role-mappings.ts b/src/management-system-v2/lib/data/db/iam/role-mappings.ts index ad287e4d5..0ce141dd2 100644 --- a/src/management-system-v2/lib/data/db/iam/role-mappings.ts +++ b/src/management-system-v2/lib/data/db/iam/role-mappings.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { v4 } from 'uuid'; import { getRoleById } from './roles'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; @@ -7,7 +8,8 @@ import { getUserById } from './users'; import { getEnvironmentById } from './environments'; import db from '@/lib/data/db'; import { Prisma } from '@prisma/client'; -import { UserFacingError } from '@/lib/user-error'; +import { UserFacingError } from '@/lib/server-error-handling/user-error'; +import { ensureTransactionWrapper } from '../util'; const RoleMappingInputSchema = z.object({ roleId: z.string(), @@ -44,7 +46,7 @@ export async function getRoleMappings(ability?: Ability, environmentId?: string) })), ); - return ability ? ability.filter('view', 'RoleMapping', roleMappings) : roleMappings; + return ok(ability ? ability.filter('view', 'RoleMapping', roleMappings) : roleMappings); } /** Returns a role mapping by user id */ @@ -96,31 +98,40 @@ export async function getRoleMappingByUserId( memberCreatedOn: role.members[0].createdOn, })); - return ability ? ability.filter('view', 'RoleMapping', userRoleMappings) : userRoleMappings; + const roleMappings = ability + ? ability.filter('view', 'RoleMapping', userRoleMappings) + : userRoleMappings; + return ok(roleMappings); } // TODO: also check if user exists? /** Adds a user role mapping */ -export async function addRoleMappings( +export const addRoleMappings = ensureTransactionWrapper(_addRoleMappings, 2); +export async function _addRoleMappings( roleMappingsInput: RoleMappingInput[], ability?: Ability, _tx?: Prisma.TransactionClient, -): Promise { +) { if (ability && !ability.can('admin', 'All')) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - if (!_tx) { - return db.$transaction(async (tx) => { - return await addRoleMappings(roleMappingsInput, ability, tx); - }); - } const tx = _tx!; - const roleMappings = roleMappingsInput.map((roleMappingInput) => - RoleMappingInputSchema.parse(roleMappingInput), + const roleMappingsParseResults = roleMappingsInput.map((roleMappingInput) => + RoleMappingInputSchema.safeParse(roleMappingInput), ); + type ParsedRoleMapping = z.infer; + const parseError = roleMappingsParseResults.find((result) => !result.success && result) as + | z.SafeParseError + | undefined; + if (parseError) return err(parseError.error); + + const roleMappings = ( + roleMappingsParseResults as unknown as z.SafeParseSuccess[] + ).map((mapping) => mapping.data); + const allowedRoleMappings = ability ? ability.filter('create', 'RoleMapping', roleMappings) : roleMappings; @@ -129,26 +140,33 @@ export async function addRoleMappings( const { roleId, userId, environmentId } = roleMapping; const environment = await getEnvironmentById(environmentId, undefined, undefined, tx); - if (!environment) throw new Error(`Environment ${environmentId} doesn't exist`); - if (!environment.isOrganization) { - throw new UserFacingError('Cannot add role mapping to personal environment'); + if (environment.isErr()) { + return environment; + } + if (!environment.value) return err(new Error(`Environment ${environmentId} doesn't exist`)); + if (!environment.value.isOrganization) { + return err(new UserFacingError('Cannot add role mapping to personal environment')); } const role = await getRoleById(roleId, undefined, tx); - if (!role) throw new UserFacingError('Role not found'); + if (role.isErr()) { + return role; + } + if (!role.value) return err(new UserFacingError('Role not found')); - if (role.name === '@everyone' || role.name === '@guest') { - throw new UserFacingError(`Cannot add role mappings to ${role.name} role`); + if (role.value.name === '@everyone' || role.value.name === '@guest') { + return err(new UserFacingError(`Cannot add role mappings to ${role.value.name} role`)); } const user = await getUserById(userId, undefined, tx); - if (!user) throw new Error('User not found'); - if (user.isGuest) throw new UserFacingError('Guests cannot have role mappings'); + if (user.isErr()) return user; + if (!user.value) return err(new Error('User not found')); + if (user.value.isGuest) return err(new UserFacingError('Guests cannot have role mappings')); const existingRoleMapping = await tx.roleMember.findFirst({ where: { roleId, userId }, }); - if (existingRoleMapping) throw new UserFacingError('Role mapping already exists'); + if (existingRoleMapping) return err(new UserFacingError('Role mapping already exists')); const id = v4(); const createdOn = new Date().toISOString(); @@ -183,7 +201,7 @@ export async function deleteRoleMapping( ability?: Ability, ) { if (ability && !ability.can('admin', 'All')) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } const [environment, user, roleMapping, role] = await Promise.all([ @@ -192,34 +210,43 @@ export async function deleteRoleMapping( getRoleMappingByUserId(userId, environmentId, ability, roleId), getRoleById(roleId), ]); - if (!environment) throw new Error("Environment doesn't exist"); + if (environment.isErr()) return environment; + if (!environment.value) return err(new Error("Environment doesn't exist")); - if (!user) throw new Error("User doesn't exist"); + if (user.isErr()) return user; + if (!user.value) return err(new Error("User doesn't exist")); - if (!roleMapping[0]) throw new Error("Role mapping doesn't exist"); + if (role.isErr()) return role; + + if (roleMapping.isErr()) return roleMapping; + if (!roleMapping.value[0]) return err(new Error("Role mapping doesn't exist")); // Check ability if ( ability && !ability.can('delete', toCaslResource('RoleMapping', roleMapping), { environmentId }) ) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - if (role!.name === '@admin') { + if (role.value!.name === '@admin') { const memberIds = await db.roleMember.findMany({ where: { roleId }, select: { userId: true }, }); if (memberIds.length === 1) { - throw new UserFacingError( - 'Cannot remove user from @admin role, at least one user must be in the role.', + return err( + new UserFacingError( + 'Cannot remove user from @admin role, at least one user must be in the role.', + ), ); } } await db.roleMember.delete({ - where: { id: roleMapping[0].id }, + where: { id: roleMapping.value[0].id }, }); + + return ok(); } diff --git a/src/management-system-v2/lib/data/db/iam/roles.ts b/src/management-system-v2/lib/data/db/iam/roles.ts index d07131af1..c3507347a 100644 --- a/src/management-system-v2/lib/data/db/iam/roles.ts +++ b/src/management-system-v2/lib/data/db/iam/roles.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { v4 } from 'uuid'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { toCaslResource } from '@/lib/ability/caslAbility'; @@ -14,7 +15,7 @@ export async function getRoles(environmentId?: string, ability?: Ability) { const filteredRoles = ability ? ability.filter('view', 'Role', roles) : roles; - return filteredRoles as Role[]; + return ok(filteredRoles as Role[]); } /** Returns all roles in form of an array including the members of each role included in its data */ @@ -49,7 +50,7 @@ export async function getRolesWithMembers(environmentId?: string, ability?: Abil .map((role) => ({ ...role, members: ability.filter('view', 'User', role.members) })) : mappedRoles; - return filteredRoles; + return ok(filteredRoles); } /** @@ -65,13 +66,13 @@ export async function getRoleByName(environmentId: string, name: string, ability }, }); - if (!role) return undefined; + if (!role) return ok(undefined); if (ability && !ability.can('view', toCaslResource('Role', role))) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } - return role; + return ok(role); } /** @@ -91,11 +92,12 @@ export async function getRoleById( }, }); - if (!ability) return role as Role; + if (!ability) return ok(role as Role); - if (role && !ability.can('view', toCaslResource('Role', role))) throw new UnauthorizedError(); + if (role && !ability.can('view', toCaslResource('Role', role))) + return err(new UnauthorizedError()); - return role as Role; + return ok(role as Role | null); } /** @@ -125,21 +127,23 @@ export async function getRoleWithMembersById(roleId: string, ability?: Ability) }, }); - if (!role) return null; + if (!role) return ok(null); const mappedRole = { ...role, members: role.members.map((member) => member.user), } as RoleWithMembers; - if (!ability) return mappedRole; + if (!ability) return ok(mappedRole); if (mappedRole && !ability.can('view', toCaslResource('Role', mappedRole))) - throw new UnauthorizedError(); + return err(new UnauthorizedError()); - return ability + const filteredRoles = ability ? { ...mappedRole, members: ability.filter('view', 'User', mappedRole.members) } : mappedRole; + + return ok(filteredRoles); } /** @@ -159,7 +163,7 @@ export async function getUserRoles(userId: string, environmentId?: string, abili const filteredRoles = ability ? ability.filter('view', 'Role', roles) : roles; - return filteredRoles as Role[]; + return ok(filteredRoles as Role[]); } /** @@ -175,7 +179,12 @@ export async function addRole( ) { const dbMutator = tx ? tx : db; - const roleRepresentation = RoleInputSchema.parse(roleRepresentationInput); + const parseResult = RoleInputSchema.safeParse(roleRepresentationInput); + if (!parseResult.success) { + return err(parseResult.error); + } + + const roleRepresentation = parseResult.data; // if (ability && !ability.can('create', toCaslResource('Role', roleRepresentation))) { if ( @@ -183,7 +192,7 @@ export async function addRole( (!ability.can('create', toCaslResource('Role', roleRepresentation)) || !ability.can('admin', 'All')) ) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } const { name, description, note, permissions, expiration, environmentId } = roleRepresentation; @@ -197,7 +206,7 @@ export async function addRole( }); if (existingRole) { - throw new Error('Role already exists'); + return err(new Error('Role already exists')); } const createdOn = new Date().toISOString(); @@ -219,7 +228,7 @@ export async function addRole( }, }); - return createdRole as Role; + return ok(createdRole as Role); } /** @@ -234,21 +243,28 @@ export async function updateRole( ability: Ability, ) { const targetRole = await getRoleById(roleId); - if (!targetRole) throw new Error('Role not found'); + if (targetRole.isErr()) return targetRole; + if (!targetRole.value) return err(new Error('Role not found')); - const roleRepresentation = RoleInputSchema.partial().parse(roleRepresentationInput); + const parseResult = RoleInputSchema.partial().safeParse(roleRepresentationInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const roleRepresentation = parseResult.data; // Casl isn't really built to check the value of input fields when updating, so we have to perform this two checks if ( !( ability.checkInputFields(toCaslResource('Role', targetRole), 'update', roleRepresentation) && ability.can('create', toCaslResource('Role', roleRepresentation), { - environmentId: targetRole.environmentId, + environmentId: targetRole.value.environmentId, }) ) || !ability.can('admin', 'All') - ) - throw new UnauthorizedError(); + ) { + return err(new UnauthorizedError()); + } + const updatedRole = await db.role.update({ where: { id: roleId, @@ -258,9 +274,10 @@ export async function updateRole( lastEditedOn: new Date().toISOString(), }, }); + rulesCacheDeleteAll(); - return updatedRole as Role; + return ok(updatedRole as Role); } /** @@ -278,7 +295,7 @@ export async function deleteRole(roleId: string, ability?: Ability) { // Throw error if role not found if (!role) { - throw new Error('Role not found'); + return err(new Error('Role not found')); } // Check if user has permission to delete the role @@ -286,7 +303,7 @@ export async function deleteRole(roleId: string, ability?: Ability) { ability && (!ability.can('delete', toCaslResource('Role', role)) || !ability.can('admin', 'All')) ) { - throw new UnauthorizedError(); + return err(new UnauthorizedError()); } // Delete role from database @@ -296,5 +313,5 @@ export async function deleteRole(roleId: string, ability?: Ability) { }, }); - return true; + return ok(true); } diff --git a/src/management-system-v2/lib/data/db/iam/system-admins.ts b/src/management-system-v2/lib/data/db/iam/system-admins.ts index de5ef62bd..d6315fce2 100644 --- a/src/management-system-v2/lib/data/db/iam/system-admins.ts +++ b/src/management-system-v2/lib/data/db/iam/system-admins.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { v4 } from 'uuid'; import { SystemAdmin, @@ -7,21 +8,20 @@ import { SystemAdminUpdateInput, SystemAdminUpdateInputSchema, } from '../../system-admin-schema'; -import { enableUseDB } from 'FeatureFlags'; import db from '@/lib/data/db'; import { Prisma } from '@prisma/client'; export async function getSystemAdmins() { const sysAdmins = await db.systemAdmin.findMany({}); - return sysAdmins as SystemAdmin[]; + return ok(sysAdmins as SystemAdmin[]); } export async function getAdminById(id: string) { - return await db.systemAdmin.findUnique({ where: { id: id } }); + return ok(await db.systemAdmin.findUnique({ where: { id: id } })); } export async function getSystemAdminByUserId(userId: string) { - return (await db.systemAdmin.findUnique({ where: { userId: userId } })) as SystemAdmin; + return ok((await db.systemAdmin.findUnique({ where: { userId: userId } })) as SystemAdmin); } export async function addSystemAdmin( @@ -42,7 +42,7 @@ export async function addSystemAdmin( await dbMutator.systemAdmin.create({ data: { ...admin } }); - return admin; + return ok(admin); } export async function updateSystemAdmin( @@ -51,23 +51,31 @@ export async function updateSystemAdmin( ) { // TODO: decide if permissions should be checkded here const adminData = await getAdminById(adminId); - if (!adminData) throw new Error('System admin not found'); + if (adminData.isErr()) { + return adminData; + } + if (!adminData.value) return err(new Error('System admin not found')); + const parseResult = SystemAdminUpdateInputSchema.partial().safeParse(adminUpdate); + if (!parseResult.success) { + return err(parseResult.error); + } const newAdminData = { - ...adminData, - ...SystemAdminUpdateInputSchema.partial().parse(adminUpdate), + ...adminData.value, + ...parseResult.data, lastEditedOn: new Date(), } as SystemAdmin; await db.systemAdmin.update({ where: { id: adminId }, data: { ...newAdminData } }); - return newAdminData; + return ok(newAdminData); } export async function deleteSystemAdmin(adminId: string) { // TODO: decide if permissions should be checkded here const adminData = await getAdminById(adminId); - if (!adminData) throw new Error('System admin not found'); + if (adminData.isErr()) return adminData; + if (!adminData.value) return err(new Error('System admin not found')); await db.systemAdmin.delete({ where: { id: adminId } }); } diff --git a/src/management-system-v2/lib/data/db/iam/users.ts b/src/management-system-v2/lib/data/db/iam/users.ts index a0099f8a6..a4c31edf3 100644 --- a/src/management-system-v2/lib/data/db/iam/users.ts +++ b/src/management-system-v2/lib/data/db/iam/users.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { v4 } from 'uuid'; import { User, @@ -13,9 +14,10 @@ import { getUserOrganizationEnvironments } from './memberships'; import { getRoleMappingByUserId } from './role-mappings'; import db from '@/lib/data/db'; import { Prisma, PasswordAccount } from '@prisma/client'; -import { UserFacingError } from '@/lib/user-error'; +import { UserFacingError } from '@/lib/server-error-handling/user-error'; import { env } from '@/lib/ms-config/env-vars'; import { NextAuthEmailTakenError, NextAuthUsernameTakenError } from '@/lib/authjs-error-message'; +import { ensureTransactionWrapper } from '../util'; export async function getUsers(page: number = 1, pageSize: number = 10) { // TODO ability check @@ -28,7 +30,7 @@ export async function getUsers(page: number = 1, pageSize: number = 10) { const totalUsers = await db.user.count(); const totalPages = Math.ceil(totalUsers / pageSize); - return { + return ok({ users, pagination: { currentPage: page, @@ -36,7 +38,7 @@ export async function getUsers(page: number = 1, pageSize: number = 10) { totalUsers, totalPages, }, - }; + }); } export async function getUserById( @@ -48,36 +50,39 @@ export async function getUserById( const user = await dbMutator.user.findUnique({ where: { id: id } }); - if (!user && opts && opts.throwIfNotFound) throw new Error('User not found'); + if (!user && opts && opts.throwIfNotFound) return err(new Error('User not found')); - return user as User; + return ok(user as User); } -export async function getUserByEmail(email: string, opts?: { throwIfNotFound?: boolean }) { +export async function getUserByEmail(email: string) { const user = await db.user.findUnique({ where: { email: email } }); - if (!user && opts?.throwIfNotFound) throw new Error('User not found'); + if (!user) return err(new Error('User not found')); - return user as User; + return ok(user as User); } export async function getUserByUsername(username: string, opts?: { throwIfNotFound?: boolean }) { const user = await db.user.findUnique({ where: { username } }); - if (!user && opts?.throwIfNotFound) throw new Error('User not found'); + if (!user && opts?.throwIfNotFound) return err(new Error('User not found')); - return user as User; + return ok(user as User); } -export async function addUser( +export const addUser = ensureTransactionWrapper(_addUser, 1); +export async function _addUser( inputUser: OptionalKeys, - tx?: Prisma.TransactionClient, -): Promise { - if (!tx) { - return await db.$transaction(async (trx: Prisma.TransactionClient) => addUser(inputUser, trx)); - } + _tx?: Prisma.TransactionClient, +) { + const tx = _tx!; - const user = UserSchema.parse(inputUser); + const parseResult = UserSchema.safeParse(inputUser); + if (!parseResult.success) { + return err(parseResult.error); + } + const user = parseResult.data; if (!user.isGuest) { const checks = []; @@ -87,10 +92,10 @@ export async function addUser( const [usernameRes, emailRes] = await Promise.all(checks); if (usernameRes) { - throw new NextAuthUsernameTakenError(); + return err(new NextAuthUsernameTakenError()); } if (emailRes) { - throw new NextAuthEmailTakenError(); + return err(new NextAuthEmailTakenError()); } } @@ -98,7 +103,7 @@ export async function addUser( try { const userExists = await tx.user.findUnique({ where: { id: user.id } }); - if (userExists) throw new Error('User already exists'); + if (userExists) return err(new Error('User already exists')); await tx.user.create({ data: { @@ -107,8 +112,16 @@ export async function addUser( }, }); - if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) - await addEnvironment({ ownerId: user.id!, isOrganization: false }, undefined, tx); + if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { + const personalSpace = await addEnvironment( + { ownerId: user.id!, isOrganization: false }, + undefined, + tx, + ); + if (personalSpace.isErr()) { + return personalSpace; + } + } if (user.isGuest) { await tx.guestSignin.create({ @@ -121,7 +134,7 @@ export async function addUser( console.error('Error adding new user: ', error); } - return user as User; + return ok(user as User); } export class UserHasToDeleteOrganizationsError extends Error { @@ -134,35 +147,38 @@ export class UserHasToDeleteOrganizationsError extends Error { } } -export async function deleteUser(userId: string, tx?: Prisma.TransactionClient): Promise { - // if no tx, start own transaction - if (!tx) { - return await db.$transaction(async (trx: Prisma.TransactionClient) => { - return await deleteUser(userId, trx); - }); - } - - const dbMutator = tx; +export const deleteUser = ensureTransactionWrapper(_deleteUser, 1); +export async function _deleteUser(userId: string, tx?: Prisma.TransactionClient) { + const dbMutator = tx!; const user = await db.user.findUnique({ where: { id: userId } }); - if (!user) throw new Error("User doesn't exist"); + if (!user) return err(new Error("User doesn't exist")); if (user.username === 'admin') { - throw new UserFacingError('The user "admin" cannot be deleted'); + return err(new UserFacingError('The user "admin" cannot be deleted')); } const userOrganizations = await getUserOrganizationEnvironments(userId); + if (userOrganizations.isErr()) { + return userOrganizations; + } + const orgsWithNoNextAdmin: string[] = []; - for (const environmentId of userOrganizations) { + for (const environmentId of userOrganizations.value) { const userRoles = await getRoleMappingByUserId(userId, environmentId); + if (userRoles.isErr()) { + return userRoles; + } - if (!userRoles.find((role) => role.roleName === '@admin')) continue; + if (!userRoles.value.find((role) => role.roleName === '@admin')) continue; const adminRole = await db.role.findFirst({ where: { name: '@admin', environmentId: environmentId }, include: { members: true }, }); if (!adminRole) - throw new Error(`Consistency error: admin role of environment ${environmentId} not found`); + return err( + new Error(`Consistency error: admin role of environment ${environmentId} not found`), + ); if (adminRole.members.length === 1) { orgsWithNoNextAdmin.push(environmentId); @@ -177,7 +193,7 @@ export async function deleteUser(userId: string, tx?: Prisma.TransactionClient): } if (orgsWithNoNextAdmin.length > 0) - throw new UserHasToDeleteOrganizationsError(orgsWithNoNextAdmin); + return err(new UserHasToDeleteOrganizationsError(orgsWithNoNextAdmin)); if (user.isGuest) { await dbMutator.guestSignin.delete({ where: { userId: userId } }); @@ -185,7 +201,7 @@ export async function deleteUser(userId: string, tx?: Prisma.TransactionClient): await dbMutator.user.delete({ where: { id: userId } }); - return user as User; + return ok(user as User); } export async function updateUser( @@ -194,37 +210,47 @@ export async function updateUser( tx?: Prisma.TransactionClient, ) { const dbMutator = tx || db; + const user = await getUserById(userId, { throwIfNotFound: true }); - const isGoingToBeGuest = inputUser.isGuest !== undefined ? inputUser.isGuest : user?.isGuest; + if (user.isErr()) { + return user; + } + + const isGoingToBeGuest = + inputUser.isGuest !== undefined ? inputUser.isGuest : user.value?.isGuest; let updatedUser: Prisma.UserUpdateInput; if (isGoingToBeGuest) { if (inputUser.username || inputUser.lastName || inputUser.firstName || inputUser.email) { - throw new Error('Guest users cannot update their user data'); + return err(new Error('Guest users cannot update their user data')); } updatedUser = { isGuest: true }; } else { - const newUserData = AuthenticatedUserSchema.partial().parse(inputUser); + const parseResult = AuthenticatedUserSchema.partial().safeParse(inputUser); + if (!parseResult.success) { + return err(parseResult.error); + } + const newUserData = parseResult.data; if (newUserData.username && newUserData.username === 'admin') { - throw new UserFacingError('The username is already taken'); + return err(new UserFacingError('The username is already taken')); } - if (!user.isGuest && user.username === 'admin' && 'username' in newUserData) { - throw new UserFacingError('The username "admin" cannot be changed'); + if (!user.value.isGuest && user.value.username === 'admin' && 'username' in newUserData) { + return err(new UserFacingError('The username "admin" cannot be changed')); } if (newUserData.email) { const existingUser = await db.user.findUnique({ where: { email: newUserData.email } }); if (existingUser && existingUser.id !== userId) - throw new UserFacingError('User with this email or username already exists'); + return err(new UserFacingError('User with this email or username already exists')); } if (newUserData.username) { const existingUser = await db.user.findUnique({ where: { username: newUserData.username } }); if (existingUser && existingUser.id !== userId) - throw new UserFacingError('The username is already taken'); + return err(new UserFacingError('The username is already taken')); } updatedUser = { ...user, ...newUserData }; @@ -235,15 +261,16 @@ export async function updateUser( data: updatedUser, }); - return updatedUserFromDB; + return ok(updatedUserFromDB); } export async function addOauthAccount(accountInput: Omit) { const newAccount = OauthAccountSchema.parse(accountInput); const user = await getUserById(newAccount.userId); - if (!user) throw new Error('User not found'); - if (user.isGuest) throw new Error('Guest users cannot have oauth accounts'); + if (user.isErr()) return user; + if (!user.value) return err(new Error('User not found')); + if (user.value.isGuest) return err(new Error('Guest users cannot have oauth accounts')); const id = v4(); @@ -251,7 +278,7 @@ export async function addOauthAccount(accountInput: Omit) { await db.oauthAccount.create({ data: account }); - return account; + return ok(account); } export async function deleteOauthAccount(id: string) { @@ -262,12 +289,14 @@ export async function deleteOauthAccount(id: string) { }); } export async function getOauthAccountByProviderId(provider: string, providerAccountId: string) { - return await db.oauthAccount.findUnique({ - where: { - provider: provider, - providerAccountId: providerAccountId, - }, - }); + return ok( + await db.oauthAccount.findUnique({ + where: { + provider: provider, + providerAccountId: providerAccountId, + }, + }), + ); } export async function updateGuestUserLastSigninTime( @@ -276,25 +305,28 @@ export async function updateGuestUserLastSigninTime( tx?: Prisma.TransactionClient, ) { const dbMutator = tx || db; - const user = await getUserById(userId, { throwIfNotFound: true }); - if (!user.isGuest) throw new Error('User is not a guest user'); - - return await dbMutator.guestSignin.update({ - where: { userId: userId }, - data: { lastSigninAt: date }, + const user = await dbMutator.user.findUnique({ + where: { id: userId }, + select: { isGuest: true }, }); + + if (!user) return err(new Error('User does not exist')); + if (!user.isGuest) return err(new Error('User is not a guest user')); + + return ok( + await dbMutator.guestSignin.update({ + where: { userId: userId }, + data: { lastSigninAt: date }, + }), + ); } -export async function deleteInactiveGuestUsers( +export const deleteInactiveGuestUsers = ensureTransactionWrapper(_deleteInactiveGuestUsers, 1); +export async function _deleteInactiveGuestUsers( inactiveTimeInMS: number, - tx?: Prisma.TransactionClient, -): Promise<{ count: number }> { - // if no tx, start own transaction - if (!tx) { - return await db.$transaction(async (trx: Prisma.TransactionClient) => { - return await deleteInactiveGuestUsers(inactiveTimeInMS, trx); - }); - } + _tx?: Prisma.TransactionClient, +) { + const tx = _tx!; const cutoff = new Date(Date.now() - inactiveTimeInMS); const staleSignins = await tx.guestSignin.findMany({ @@ -303,7 +335,7 @@ export async function deleteInactiveGuestUsers( }, select: { userId: true }, }); - if (staleSignins.length === 0) return { count: 0 }; + if (staleSignins.length === 0) return ok({ count: 0 }); const userIds = staleSignins.map((s) => s.userId); @@ -313,12 +345,14 @@ export async function deleteInactiveGuestUsers( }, }); - return await tx.user.deleteMany({ - where: { - id: { in: userIds }, - isGuest: true, - }, - }); + return ok( + await tx.user.deleteMany({ + where: { + id: { in: userIds }, + isGuest: true, + }, + }), + ); } /** Note: make sure to save a salted hash of the password */ @@ -334,7 +368,7 @@ export async function setUserPassword( where: { id: userId }, include: { passwordAccount: true }, }); - if (!user) throw new Error('User not found'); + if (!user) return err(new Error('User not found')); if (user.passwordAccount) { await dbMutator.passwordAccount.update({ @@ -346,14 +380,17 @@ export async function setUserPassword( data: { userId, password: passwordHash, isTemporaryPassword }, }); } + return ok(); } export async function getUserPassword(userId: string, tx?: Prisma.TransactionClient) { const dbMutator = tx || db; - return await dbMutator.passwordAccount.findUnique({ - where: { userId }, - }); + return ok( + await dbMutator.passwordAccount.findUnique({ + where: { userId }, + }), + ); } /** returns null if the user exists but has no password */ @@ -368,7 +405,7 @@ export async function getUserAndPasswordByUsername( include: { passwordAccount: true }, }); - if (!userAndPassword) return null; - if (!userAndPassword.passwordAccount) return null; - return userAndPassword as typeof userAndPassword & { passwordAccount: PasswordAccount }; + if (!userAndPassword) return ok(null); + if (!userAndPassword.passwordAccount) return ok(null); + return ok(userAndPassword as typeof userAndPassword & { passwordAccount: PasswordAccount }); } diff --git a/src/management-system-v2/lib/data/db/iam/verification-tokens.ts b/src/management-system-v2/lib/data/db/iam/verification-tokens.ts index 9e35cf222..cbedff185 100644 --- a/src/management-system-v2/lib/data/db/iam/verification-tokens.ts +++ b/src/management-system-v2/lib/data/db/iam/verification-tokens.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { z } from 'zod'; import db from '@/lib/data/db'; import { Prisma } from '@prisma/client'; @@ -34,12 +35,14 @@ export async function getEmailVerificationToken({ token: string; identifier: string; }) { - return (await db.emailVerificationToken.findFirst({ - where: { - token, - identifier, - }, - })) as EmailVerificationToken | null; + return ok( + (await db.emailVerificationToken.findFirst({ + where: { + token, + identifier, + }, + })) as EmailVerificationToken | null, + ); } export async function deleteEmailVerificationToken({ @@ -53,17 +56,23 @@ export async function deleteEmailVerificationToken({ where: { token, identifier }, }); - if (!result) throw new Error('Token not found'); + if (!result) return err(new Error('Token not found')); - return result; + return ok(result); } export async function saveEmailVerificationToken(tokenInput: EmailVerificationToken) { - const token = emailVerificationTokenSchemam.parse(tokenInput); + const parseResult = emailVerificationTokenSchemam.safeParse(tokenInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const token = parseResult.data; - return await db.emailVerificationToken.create({ - data: token, - }); + return ok( + await db.emailVerificationToken.create({ + data: token, + }), + ); } export async function updateEmailVerificationTokenExpiration( @@ -76,10 +85,12 @@ export async function updateEmailVerificationTokenExpiration( ) { const mutator = tx || db; - return await mutator.emailVerificationToken.update({ - where: tokenIdentifier, - data: { - expires: newExpiration, - }, - }); + return ok( + await mutator.emailVerificationToken.update({ + where: tokenIdentifier, + data: { + expires: newExpiration, + }, + }), + ); } diff --git a/src/management-system-v2/lib/data/db/process.ts b/src/management-system-v2/lib/data/db/process.ts index 713a1f0ee..646daea70 100644 --- a/src/management-system-v2/lib/data/db/process.ts +++ b/src/management-system-v2/lib/data/db/process.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { getFolderById } from './folders'; import eventHandler from '../legacy/eventHandler.js'; import logger from '../legacy/logging.js'; @@ -26,6 +27,7 @@ import { copyFile, retrieveFile } from '../file-manager/file-manager'; import { generateProcessFilePath } from '@/lib/helpers/fileManagerHelpers'; import { Prisma } from '@prisma/client'; import { getUsedImagesFromJson } from '@/components/html-form-editor/serialized-format-utils'; +import { ensureTransactionWrapper } from './util'; /** * Returns all processes in an environment @@ -61,7 +63,7 @@ export async function getProcesses(environmentId: string, ability?: Ability, inc //TODO: add pagination - return ability ? ability.filter('view', 'Process', spaceProcesses) : spaceProcesses; + return ok(ability ? ability.filter('view', 'Process', spaceProcesses) : spaceProcesses); } export async function getProcess(processDefinitionsId: string, includeBPMN = false) { @@ -92,7 +94,7 @@ export async function getProcess(processDefinitionsId: string, includeBPMN = fal }, }); if (!process) { - throw new Error(`Process with id ${processDefinitionsId} could not be found!`); + return err(new Error(`Process with id ${processDefinitionsId} could not be found!`)); } // Convert BigInt fields to number @@ -120,9 +122,11 @@ export async function getProcess(processDefinitionsId: string, includeBPMN = fal // TODO: implement inEditingBy }; - return convertedProcess as typeof convertedProcess & { - inEditingBy?: { id: string; task?: string }[]; - }; + return ok( + convertedProcess as typeof convertedProcess & { + inEditingBy?: { id: string; task?: string }[]; + }, + ); } /** @@ -140,9 +144,9 @@ export async function checkIfProcessExists( }, }); if (!existingProcess && throwError) { - throw new Error(`Process with id ${processDefinitionsId} does not exist!`); + return err(new Error(`Process with id ${processDefinitionsId} does not exist!`)); } - return existingProcess; + return ok(existingProcess); } export async function checkIfProcessAlreadyExistsForAUserInASpaceByName( @@ -163,9 +167,9 @@ export async function checkIfProcessAlreadyExistsForAUserInASpaceByName( }, }); - return !!existingProcess; + return ok(!!existingProcess); } catch (err: any) { - throw new Error('Error checking if process exists by name:', err.message); + return err(new Error('Error checking if process exists by name:', err.message)); } } @@ -198,32 +202,30 @@ export async function checkIfProcessAlreadyExistsForAUserInASpaceByNameWithBatch const existingSet = new Set(existingProcesses.map((p) => `${p.name}:::${p.folderId}`)); // Return an array of booleans per process - return processes.map(({ name, folderId }) => { - return existingSet.has(`${name}:::${folderId}`); - }); + return ok(processes.map(({ name, folderId }) => existingSet.has(`${name}:::${folderId}`))); } catch (err: any) { - throw new Error(`Error checking process names in batch: ${err.message}`); + return err(new Error(`Error checking process names in batch: ${err.message}`)); } } /** Handles adding a process, makes sure all necessary information gets parsed from bpmn */ -export async function addProcess( +export const addProcess = ensureTransactionWrapper(_addProcess, 2); +export async function _addProcess( processInput: ProcessServerInput & { bpmn: string }, referencedProcessId?: string, - tx?: Prisma.TransactionClient, -): Promise { - if (!tx) { - return await db.$transaction(async (trx: Prisma.TransactionClient) => { - return await addProcess(processInput, referencedProcessId, trx); - }); - } - const { bpmn } = processInput; - - const processData = ProcessServerInputSchema.parse(processInput); + _tx?: Prisma.TransactionClient, +) { + const tx = _tx!; + const { bpmn } = processInput; if (!bpmn) { - throw new Error("Can't create a process without a bpmn!"); + return err(new Error("Can't create a process without a bpmn!")); } + const parseResult = ProcessServerInputSchema.safeParse(processInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const processData = parseResult.data; // create meta info object const metadata = { @@ -233,11 +235,14 @@ export async function addProcess( }; if (!metadata.folderId) { - metadata.folderId = (await getRootFolder(metadata.environmentId)).id; + const rootFolder = await getRootFolder(metadata.environmentId); + if (rootFolder.isErr()) return rootFolder; + metadata.folderId = rootFolder.value.id; } const folderData = await getFolderById(metadata.folderId); - if (!folderData) throw new Error('Folder not found'); + if (folderData.isErr()) return folderData; + if (!folderData) return err(new Error('Folder not found')); // TODO check folder permissions here, they're checked in movefolder, // but by then the folder was already created @@ -250,7 +255,7 @@ export async function addProcess( }, }); if (existingProcess) { - throw new Error(`Process with id ${processDefinitionsId} already exists!`); + return err(new Error(`Process with id ${processDefinitionsId} already exists!`)); } const bpmnWithPlaceholders = await transformBpmnAttributes( @@ -259,32 +264,28 @@ export async function addProcess( ); // save process info - try { - await tx.process.create({ - data: { - id: metadata.id, - originalId: metadata.originalId ?? '', - name: metadata.name, - description: metadata.description, - createdOn: new Date().toISOString(), - lastEditedOn: new Date().toISOString(), - type: metadata.type, - processIds: { set: metadata.processIds }, - folderId: metadata.folderId, - sharedAs: metadata.sharedAs, - shareTimestamp: metadata.shareTimestamp, - allowIframeTimestamp: metadata.allowIframeTimestamp, - environmentId: metadata.environmentId, - creatorId: metadata.creatorId, - userDefinedId: metadata.userDefinedId, - //departments: { set: metadata.departments }, - //variables: { set: metadata.variables }, - bpmn: bpmnWithPlaceholders, - }, - }); - } catch (error) { - console.error('Error adding new process: ', error); - } + await tx.process.create({ + data: { + id: metadata.id, + originalId: metadata.originalId ?? '', + name: metadata.name, + description: metadata.description, + createdOn: new Date().toISOString(), + lastEditedOn: new Date().toISOString(), + type: metadata.type, + processIds: { set: metadata.processIds }, + folderId: metadata.folderId, + sharedAs: metadata.sharedAs, + shareTimestamp: metadata.shareTimestamp, + allowIframeTimestamp: metadata.allowIframeTimestamp, + environmentId: metadata.environmentId, + creatorId: metadata.creatorId, + userDefinedId: metadata.userDefinedId, + //departments: { set: metadata.departments }, + //variables: { set: metadata.variables }, + bpmn: bpmnWithPlaceholders, + }, + }); //if referencedProcessId is present, the process was copied from a shared process if (referencedProcessId) { @@ -302,18 +303,27 @@ export async function addProcess( eventHandler.dispatch('processAdded', { process: metadata }); - return metadata; + return ok(metadata as ProcessMetadata); } /** Updates an existing process with the given bpmn */ -export async function updateProcess( +export const updateProcess = ensureTransactionWrapper(_updateProcess, 2); +export async function _updateProcess( processDefinitionsId: string, newInfoInput: Partial & { bpmn?: string }, + _tx?: Prisma.TransactionClient, ) { const { bpmn: newBpmn } = newInfoInput; - const newInfo = ProcessServerInputSchema.partial().parse(newInfoInput); - checkIfProcessExists(processDefinitionsId); - const currentParent = (await getProcess(processDefinitionsId)).folderId; + const parseResult = ProcessServerInputSchema.partial().safeParse(newInfoInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const newInfo = parseResult.data; + + const process = await getProcess(processDefinitionsId); + if (process.isErr()) return process; + + const currentParent = process.value.folderId; let metaChanges = { ...newInfo, @@ -329,11 +339,16 @@ export async function updateProcess( // Update folders if (metaChanges.folderId && metaChanges.folderId !== currentParent) { - moveProcess({ processDefinitionsId, newFolderId: metaChanges.folderId }); + const moveResult = await moveProcess({ + processDefinitionsId, + newFolderId: metaChanges.folderId, + }); + if (moveResult?.isErr()) return moveResult; //delete metaChanges.folderId; } const newMetaData = await updateProcessMetaData(processDefinitionsId, metaChanges); + if (newMetaData?.isErr()) return newMetaData; if (newBpmn) { try { await db.process.update({ @@ -350,7 +365,7 @@ export async function updateProcess( }); } - return newMetaData; + return ok(newMetaData); } export async function moveProcess({ @@ -369,11 +384,14 @@ export async function moveProcess({ const dbMutator = tx || db; try { const process = await getProcess(processDefinitionsId); + if (process.isErr()) { + return process; + } if (!process) { - throw new Error('Process not found'); + return err(new Error('Process not found')); } - const oldFolderId = process.folderId; + const oldFolderId = process.value.folderId; const [oldFolder, newFolder] = await Promise.all([ db.folder.findUnique({ where: { id: oldFolderId! }, @@ -386,22 +404,22 @@ export async function moveProcess({ ]); if (!oldFolder) { - throw new Error("Consistency Error: Process' old folder not found"); + return err(new Error("Consistency Error: Process' old folder not found")); } if (!newFolder) { - throw new Error('New folder not found'); + return err(new Error('New folder not found')); } // Permission checks if ( ability && !( - ability.can('update', toCaslResource('Process', process)) && + ability.can('update', toCaslResource('Process', process.value)) && ability.can('update', toCaslResource('Folder', oldFolder)) && ability.can('update', toCaslResource('Folder', newFolder)) ) ) { - throw new Error('Unauthorized'); + return err(new Error('Unauthorized')); } // Update process' folderId in the database @@ -411,9 +429,10 @@ export async function moveProcess({ folderId: newFolderId, }, }); - return updatedProcess; + return ok(updatedProcess); } catch (error) { console.error('Error moving process:', error); + return err(error); } } @@ -424,7 +443,12 @@ export async function updateProcessMetaData( metaChanges: Partial>, ) { try { - const existingProcess = await checkIfProcessExists(processDefinitionsId); + const existingProcess = await db.process.findUnique({ + where: { + id: processDefinitionsId, + }, + select: { originalId: true }, + }); const updatedProcess = await db.process.update({ where: { id: processDefinitionsId }, @@ -440,38 +464,41 @@ export async function updateProcessMetaData( updatedInfo: updatedProcess, }); - return updatedProcess; + return ok(updatedProcess); } catch (error) { console.error('Error updating process metadata:', error); + return err(error); } } /** Removes an existing process */ -export async function removeProcess(processDefinitionsId: string, tx?: Prisma.TransactionClient) { - if (!tx) { - return await db.$transaction(async (trx: Prisma.TransactionClient) => { - await removeProcess(processDefinitionsId, trx); - }); - } +export const removeProcess = ensureTransactionWrapper(_removeProcess, 1); +export async function _removeProcess(processDefinitionsId: string, _tx?: Prisma.TransactionClient) { + try { + const tx = _tx!; - const process = await tx.process.findUnique({ - where: { id: processDefinitionsId }, - include: { artifactProcessReferences: { include: { artifact: true } } }, - }); + const process = await tx.process.findUnique({ + where: { id: processDefinitionsId }, + include: { artifactProcessReferences: { include: { artifact: true } } }, + }); - if (!process) { - throw new Error(`Process with id: ${processDefinitionsId} not found`); - } + if (!process) { + return err(new Error(`Process with id: ${processDefinitionsId} not found`)); + } - await Promise.all( - process.artifactProcessReferences.map((artifactRef) => { - return deleteProcessArtifact(artifactRef.artifact.filePath, true, processDefinitionsId, tx); - }), - ); + await Promise.all( + process.artifactProcessReferences.map((artifactRef) => { + return deleteProcessArtifact(artifactRef.artifact.filePath, true, processDefinitionsId, tx); + }), + ); - await tx.process.delete({ where: { id: processDefinitionsId } }); + await tx.process.delete({ where: { id: processDefinitionsId } }); - eventHandler.dispatch('processRemoved', { processDefinitionsId }); + eventHandler.dispatch('processRemoved', { processDefinitionsId }); + } catch (error) { + console.error(error); + return err(error); + } } /** Stores a new version of an existing process */ @@ -486,28 +513,32 @@ export async function addProcessVersion( let versionInformation = await getDefinitionsVersionInformation(bpmn); if (!versionInformation) { - throw new Error('The given bpmn does not contain a version.'); + return err(new Error('The given bpmn does not contain a version.')); } const existingProcess = await getProcess(processDefinitionsId); + if (existingProcess.isErr()) { + return existingProcess; + } if (!existingProcess) { - // TODO: create the process and use the given version as the "HEAD" - throw new Error('The process for which you try to create a version does not exist'); + return err(new Error('The process for which you try to create a version does not exist')); } if ( - existingProcess.type !== 'project' && + existingProcess.value.type !== 'project' && (!versionInformation.name || !versionInformation.description) ) { - throw new Error( - 'A bpmn that should be stored as a version of a process has to contain both a version name and a version description!', + return err( + new Error( + 'A bpmn that should be stored as a version of a process has to contain both a version name and a version description!', + ), ); } // don't add a version a second time if ( - existingProcess.versions.some( + existingProcess.value.versions.some( ({ createdOn }) => toCustomUTCString(createdOn) == versionInformation.versionCreatedOn, ) ) { @@ -523,7 +554,7 @@ export async function addProcessVersion( ); if (!res.filePath) { - throw new Error('Error saving version bpmn'); + return err(new Error('Error saving version bpmn')); } try { @@ -577,14 +608,17 @@ export async function addProcessVersion( } } - await versionProcessArtifactRefs(processDefinitionsId, version.id); + const versionResult = await versionProcessArtifactRefs(processDefinitionsId, version.id); + if (versionResult?.isErr()) { + return versionResult; + } } catch (error) { console.error('Error creating version: ', error); - throw new Error('Error creating the version'); + return err(new Error('Error creating the version')); } // add information about the new version to the meta information and inform others about its existence - const newVersions = existingProcess.versions ? [...existingProcess.versions] : []; + const newVersions = existingProcess.value.versions ? [...existingProcess.value.versions] : []; //@ts-ignore newVersions.push(versionInformation); @@ -594,22 +628,25 @@ export async function addProcessVersion( /** Returns the bpmn of a specific process version */ export async function getProcessVersionBpmn(processDefinitionsId: string, versionId: string) { let existingProcess = await getProcess(processDefinitionsId); + if (existingProcess.isErr()) { + return existingProcess; + } if (!existingProcess) { - throw new Error('The process for which you try to get a version does not exist'); + return err(new Error('The process for which you try to get a version does not exist')); } - const existingVersion = existingProcess.versions?.find( + const existingVersion = existingProcess.value.versions?.find( (existingVersionInfo) => existingVersionInfo.id === versionId, ); if (!existingVersion) { - throw new Error('The version you are trying to get does not exist'); + return err(new Error('The version you are trying to get does not exist')); } const versn = await db.version.findUnique({ where: { id: versionId }, }); - return ((await retrieveFile(versn?.bpmnFilePath!, false)) as Buffer).toString('utf8'); + return ok(((await retrieveFile(versn?.bpmnFilePath!, false)) as Buffer).toString('utf8')); } /** Removes information from the meta data that would not be correct after a restart */ @@ -639,7 +676,7 @@ export async function getProcessBpmn(processDefinitionsId: string) { }); if (!process) { - throw new Error('Process not found'); + return err(new Error('Process not found')); } const processWithStringDate = { @@ -650,10 +687,10 @@ export async function getProcessBpmn(processDefinitionsId: string) { processWithStringDate!, BpmnAttributeType.ACTUAL_VALUE, ); - return bpmnWithDBValue; - } catch (err) { - logger.debug(`Error reading bpmn of process. Reason:\n${err}`); - throw new Error('Unable to find process bpmn!'); + return ok(bpmnWithDBValue); + } catch (error) { + logger.debug(`Error reading bpmn of process. Reason:\n${error}`); + return err(new Error('Unable to find process bpmn!')); } } @@ -674,7 +711,10 @@ export async function getProcessHtmlFormJSON( fileName: string, ignoreDeletedStatus = false, ) { - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId); + if (check.isErr()) { + return check; + } try { let artifact; @@ -701,11 +741,13 @@ export async function getProcessHtmlFormJSON( } if (artifact) { const jsonAsBuffer = (await retrieveFile(artifact.filePath, true)) as Buffer; - return jsonAsBuffer.toString('utf8'); + return ok(jsonAsBuffer.toString('utf8')); + } else { + return ok(); } - } catch (err) { - logger.debug(`Error getting data of process html form ${fileName}. Reason\n${err}`); - throw new Error(`Unable to get data for process html form ${fileName}!`); + } catch (error) { + logger.debug(`Error getting data of process html form ${fileName}. Reason\n${error}`); + return err(new Error(`Unable to get data for process html form ${fileName}!`)); } } @@ -717,10 +759,10 @@ export async function checkIfHtmlFormExists(processDefinitionsId: string, fileNa const htmlArtifact = await db.artifact.findUnique({ where: { fileName: `${fileName}.html` }, }); - return jsonArtifact || htmlArtifact ? { json: jsonArtifact, html: htmlArtifact } : null; + return ok(jsonArtifact || htmlArtifact ? { json: jsonArtifact, html: htmlArtifact } : null); } catch (error) { console.error(`Error checking if html form ${fileName} exists:`, error); - throw new Error(`Failed to check if html form ${fileName} exists.`); + return err(new Error(`Failed to check if html form ${fileName} exists.`)); } } @@ -754,15 +796,19 @@ export async function checkIfScriptTaskFileExists( const artifact = await db.artifact.findUnique({ where: { fileName: scriptFilenameWithExtension }, }); - return artifact; + return ok(artifact); } catch (error) { console.error('Error checking if script task file exists:', error); - throw new Error('Failed to check if script task file exists.'); + return err(new Error('Failed to check if script task file exists.')); } } export async function getHtmlForm(processDefinitionsId: string, fileName: string) { - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId); + if (check.isErr()) { + return check; + } + try { const res = await db.artifact.findFirst({ where: { @@ -788,19 +834,23 @@ export async function getHtmlForm(processDefinitionsId: string, fileName: string }); if (!res) { - throw new Error(`Unable to get html for ${fileName} from the database!`); + return err(new Error(`Unable to get html for ${fileName} from the database!`)); } const html = (await retrieveFile(res.filePath, false)).toString('utf-8'); - return html; - } catch (err) { - logger.debug(`Error getting html for ${fileName} from the database. Reason:\n${err}`); - throw new Error('Unable to get html for start form!'); + return ok(html); + } catch (error) { + logger.debug(`Error getting html for ${fileName} from the database. Reason:\n${error}`); + return err(new Error('Unable to get html for start form!')); } } export async function getProcessScriptTaskScript(processDefinitionsId: string, fileName: string) { - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId); + if (check.isErr()) { + return check; + } + try { const res = await db.artifact.findFirst({ where: { @@ -826,14 +876,14 @@ export async function getProcessScriptTaskScript(processDefinitionsId: string, f }); if (!res) { - throw new Error('Unable to get script for script task!'); + return err(new Error('Unable to get script for script task!')); } const script = (await retrieveFile(res.filePath, false)).toString('utf-8'); - return script; - } catch (err) { - logger.debug(`Error getting script of script task. Reason:\n${err}`); - throw new Error('Unable to get script for script task!'); + return ok(script); + } catch (error) { + logger.debug(`Error getting script of script task. Reason:\n${error}`); + return err(new Error('Unable to get script for script task!')); } } @@ -846,9 +896,16 @@ export async function saveProcessHtmlForm( updateImageReferences = false, ) { // TODO: Use a transaction to avoid storing inconsistent states in case of errors - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId); + if (check.isErr()) { + return check; + } + try { const res = await checkIfHtmlFormExists(processDefinitionsId, fileName); + if (res.isErr()) { + return res; + } const content = new TextEncoder().encode(json); // The code that creates versions, already handles creating new references @@ -856,11 +913,12 @@ export async function saveProcessHtmlForm( let newUsedImages = getUsedImagesFromJson(JSON.parse(json)); let removedImages = new Set(); - if (res?.json) { + if (res.value?.json) { const oldJson = await getProcessHtmlFormJSON(processDefinitionsId, fileName); - if (!oldJson) throw new Error(`Couldn't get JSON for user task ${fileName}`); + if (oldJson.isErr()) return oldJson; + if (!oldJson.value) return err(new Error(`Couldn't get JSON for user task ${fileName}`)); - const oldUsedImages = getUsedImagesFromJson(JSON.parse(oldJson)); + const oldUsedImages = getUsedImagesFromJson(JSON.parse(oldJson.value)); for (const oldImage of oldUsedImages) { if (!newUsedImages.has(oldImage)) removedImages.add(oldImage); @@ -888,7 +946,7 @@ export async function saveProcessHtmlForm( { generateNewFileName: false, versionCreatedOn, - replaceFileContentOnly: res?.json?.filePath ? true : false, + replaceFileContentOnly: res.value?.json?.filePath ? true : false, context: 'html-forms', }, ); @@ -901,15 +959,15 @@ export async function saveProcessHtmlForm( { generateNewFileName: false, versionCreatedOn: versionCreatedOn, - replaceFileContentOnly: res?.html?.filePath ? true : false, + replaceFileContentOnly: res.value?.html?.filePath ? true : false, context: 'html-forms', }, ); - return filePath; - } catch (err) { - logger.debug(`Error storing html form data for ${fileName}. Reason:\n${err}`); - throw new Error('Failed to store the html form data.'); + return ok(filePath); + } catch (error) { + logger.debug(`Error storing html form data for ${fileName}. Reason:\n${error}`); + return err(new Error('Failed to store the html form data.')); } } @@ -919,9 +977,16 @@ export async function saveProcessScriptTask( script: string, versionCreatedOn?: string, ) { - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId); + if (check.isErr()) { + return check; + } + try { const res = await checkIfScriptTaskFileExists(processDefinitionsId, filenameWithExtension); + if (res.isErr()) { + return res; + } await saveProcessArtifact( processDefinitionsId, @@ -931,35 +996,43 @@ export async function saveProcessScriptTask( { generateNewFileName: false, versionCreatedOn: versionCreatedOn, - replaceFileContentOnly: res?.filePath ? true : false, + replaceFileContentOnly: res.value?.filePath ? true : false, context: 'script-tasks', }, ); - return filenameWithExtension; - } catch (err) { - logger.debug(`Error storing script task data. Reason:\n${err}`); - throw new Error('Failed to store the script task data'); + return ok(filenameWithExtension); + } catch (error) { + logger.debug(`Error storing script task data. Reason:\n${error}`); + return err(new Error('Failed to store the script task data')); } } /** Removes a stored user task from disk */ export async function deleteHtmlForm(processDefinitionsId: string, fileName: string) { - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId); + if (check.isErr()) { + return check; + } + try { const res = await checkIfHtmlFormExists(processDefinitionsId, fileName); + if (res.isErr()) { + return res; + } let isDeleted = false; - if (res?.json) { - isDeleted = await deleteProcessArtifact(res.json.filePath, true); + if (res.value?.json) { + isDeleted = await deleteProcessArtifact(res.value.json.filePath, true); } - if (res?.html) { - isDeleted = (await deleteProcessArtifact(res.html.filePath, true)) || isDeleted; + if (res.value?.html) { + isDeleted = (await deleteProcessArtifact(res.value.html.filePath, true)) || isDeleted; } - return isDeleted; - } catch (err) { - logger.debug(`Error removing html form data. Reason:\n${err}`); + return ok(isDeleted); + } catch (error) { + logger.debug(`Error removing html form data. Reason:\n${error}`); + return err(error); } } @@ -968,14 +1041,25 @@ export async function deleteProcessScriptTask( processDefinitionsId: string, taskFileNameWithExtension: string, ) { - checkIfProcessExists(processDefinitionsId); + const processExists = await checkIfProcessExists(processDefinitionsId); + if (processExists.isErr()) { + return processExists; + } + + // Not sure what should be returned here + if (!processExists.value) return; + try { const res = await checkIfScriptTaskFileExists(processDefinitionsId, taskFileNameWithExtension); - if (res) { - return await deleteProcessArtifact(res.filePath, true); + if (res.isErr()) { + return res; } - } catch (err) { - logger.debug(`Error removing script task file. Reason:\n${err}`); + if (res.value) { + return ok(await deleteProcessArtifact(res.value?.filePath, true)); + } + } catch (error) { + logger.debug(`Error removing script task file. Reason:\n${error}`); + return err(error); } } @@ -999,7 +1083,7 @@ export async function copyProcessArtifactReferences( }), ); } catch (error) { - throw new Error('error copying process artifact references'); + return err(new Error('error copying process artifact references')); } } @@ -1020,7 +1104,7 @@ export async function versionProcessArtifactRefs(processId: string, versionId: s }), ); } catch (error) { - throw new Error('error copying process artifact references'); + return err(new Error('error copying process artifact references')); } } @@ -1075,25 +1159,29 @@ export async function copyProcessFiles(sourceProcessId: string, destinationProce }); console.log(`Successfully copied artifact with ID ${artifactId} to ${newFilename}`); - return { + return ok({ mapping: { oldFilename: artifact.fileName, newFilename: newFilename }, artifactType: artifact.artifactType, - }; + }); } catch (error) { console.error( `Failed to create new artifact for destination process: ${destinationProcessId}`, ); + return err(error); } } else { - console.warn(`Failed to copy artifact with ID ${artifactId}`); + const error = new Error(`Failed to copy artifact with ID ${artifactId}`); + console.warn(error.message); + return err(error); } }); - return oldNewFilenameMapping; + return ok(oldNewFilenameMapping); } export async function getProcessImage(processDefinitionsId: string, imageFileName: string) { - checkIfProcessExists(processDefinitionsId); + const check = await checkIfProcessExists(processDefinitionsId, true); + if (check.isErr()) return check; try { const res = await db.artifact.findFirst({ @@ -1106,13 +1194,13 @@ export async function getProcessImage(processDefinitionsId: string, imageFileNam select: { filePath: true }, }); if (!res) { - throw new Error(`Unable to get image : ${imageFileName}`); + return err(new Error(`Unable to get image : ${imageFileName}`)); } const image = (await retrieveFile(res?.filePath, false)) as Buffer; - return image; - } catch (err) { - logger.debug(`Error getting image. Reason:\n${err}`); - throw new Error(`Unable to get image : ${imageFileName}`); + return ok(image); + } catch (error) { + logger.debug(`Error getting image. Reason:\n${error}`); + return err(new Error(`Unable to get image : ${imageFileName}`)); } } diff --git a/src/management-system-v2/lib/data/db/space-settings.ts b/src/management-system-v2/lib/data/db/space-settings.ts index 920fdb1f3..f742d4d16 100644 --- a/src/management-system-v2/lib/data/db/space-settings.ts +++ b/src/management-system-v2/lib/data/db/space-settings.ts @@ -1,3 +1,4 @@ +import { ok, err } from 'neverthrow'; import { Setting, SettingGroup } from '@/app/(dashboard)/[environmentId]/settings/type-util'; import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import prisma from '@/lib/data/db'; @@ -10,7 +11,7 @@ export async function getSpaceSettingsValues( searchKey: string, ability?: Ability, ) { - if (ability && !ability.can('view', 'Setting')) throw new UnauthorizedError(); + if (ability && !ability.can('view', 'Setting')) return err(new UnauthorizedError()); const settings = await db.spaceSettings.findUnique({ where: { environmentId: spaceId }, @@ -35,7 +36,7 @@ export async function getSpaceSettingsValues( }); } - return res; + return ok(res); } export async function populateSpaceSettingsGroup( @@ -43,7 +44,7 @@ export async function populateSpaceSettingsGroup( settingsGroup: SettingGroup, ability?: Ability, ) { - if (ability && ability.can('update', 'Setting')) throw new UnauthorizedError(); + if (ability && ability.can('update', 'Setting')) return err(new UnauthorizedError()); const settings = await db.spaceSettings.findUnique({ where: { environmentId: spaceId }, @@ -65,6 +66,8 @@ export async function populateSpaceSettingsGroup( if (el && 'value' in el) el.value = value; }); + + return ok(); } export async function updateSpaceSettings( @@ -72,9 +75,9 @@ export async function updateSpaceSettings( data: Record, ability?: Ability, ) { - if (ability && !ability.can('update', 'Setting')) throw new UnauthorizedError(); + if (ability && !ability.can('update', 'Setting')) return err(new UnauthorizedError()); - prisma.$transaction(async (tx) => { + await prisma.$transaction(async (tx) => { const existingSettings = await tx.spaceSettings.findUnique({ where: { environmentId: spaceId }, }); @@ -96,4 +99,6 @@ export async function updateSpaceSettings( }, }); }); + + return ok(); } diff --git a/src/management-system-v2/lib/data/db/user-tasks.ts b/src/management-system-v2/lib/data/db/user-tasks.ts index e8696b0df..311772aa1 100644 --- a/src/management-system-v2/lib/data/db/user-tasks.ts +++ b/src/management-system-v2/lib/data/db/user-tasks.ts @@ -1,3 +1,4 @@ +import { err, ok } from 'neverthrow'; import db from '@/lib/data/db'; import { z } from 'zod'; import { UserTask, UserTaskInput, UserTaskInputSchema } from '@/lib/user-task-schema'; @@ -5,10 +6,12 @@ import { UserTask, UserTaskInput, UserTaskInputSchema } from '@/lib/user-task-sc export async function getUserTasks() { const userTasks = await db.userTask.findMany(); - return userTasks.map((userTask) => ({ - ...userTask, - offline: userTask.machineId !== 'ms-local', - })) as unknown as UserTask[]; + return ok( + userTasks.map((userTask) => ({ + ...userTask, + offline: userTask.machineId !== 'ms-local', + })) as unknown as UserTask[], + ); } export async function getUserTaskById(userTaskId: string) { @@ -18,26 +21,32 @@ export async function getUserTaskById(userTaskId: string) { }, }); - if (!userTask) return undefined; + if (!userTask) return ok(undefined); // TODO: maybe handle view capability for specific user tasks - return { ...userTask, offline: true } as unknown as UserTask; + return ok({ ...userTask, offline: true } as unknown as UserTask); } const UserTaskArraySchema = UserTaskInputSchema.array(); export async function addUserTasks(userTaskInput: UserTaskInput[]) { - const newUserTasks = UserTaskArraySchema.parse(userTaskInput); + const parseResult = UserTaskArraySchema.safeParse(userTaskInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const newUserTasks = parseResult.data; // TODO: maybe check if the user can work on/add user tasks - return await db.userTask.createMany({ - data: newUserTasks.map((task) => ({ - ...task, - startTime: new Date(task.startTime), - endTime: typeof task.endTime !== 'number' ? undefined : new Date(task.endTime), - })), - }); + return ok( + await db.userTask.createMany({ + data: newUserTasks.map((task) => ({ + ...task, + startTime: new Date(task.startTime), + endTime: typeof task.endTime !== 'number' ? undefined : new Date(task.endTime), + })), + }), + ); } const PartialUserTaskInputSchema = UserTaskInputSchema.partial(); @@ -49,7 +58,11 @@ type PartialDatabaseUserTaskInput = Omit< endTime?: Date; }; export async function updateUserTask(userTaskId: string, userTaskInput: Partial) { - const newUserTaskData = PartialUserTaskInputSchema.parse(userTaskInput); + const parseResult = PartialUserTaskInputSchema.safeParse(userTaskInput); + if (!parseResult.success) { + return err(parseResult.error); + } + const newUserTaskData = parseResult.data; const updateData: PartialDatabaseUserTaskInput = { ...newUserTaskData, @@ -68,19 +81,23 @@ export async function updateUserTask(userTaskId: string, userTaskInput: Partial< // TODO: maybe check if a user is allowed to edit a user task - return await db.userTask.update({ - data: updateData, - where: { - id: userTaskId, - }, - }); + return ok( + await db.userTask.update({ + data: updateData, + where: { + id: userTaskId, + }, + }), + ); } export async function deleteUserTask(userTaskId: string) { // TODO: check if a user is allowed to delete a user task - return await db.userTask.delete({ - where: { - id: userTaskId, - }, - }); + return ok( + await db.userTask.delete({ + where: { + id: userTaskId, + }, + }), + ); } diff --git a/src/management-system-v2/lib/data/db/util.ts b/src/management-system-v2/lib/data/db/util.ts new file mode 100644 index 000000000..967127c56 --- /dev/null +++ b/src/management-system-v2/lib/data/db/util.ts @@ -0,0 +1,30 @@ +import db from '@/lib/data/db'; +import { Err, err } from 'neverthrow'; + +export function ensureTransactionWrapper | unknown, Args extends any[]>( + fn: (...args: Args) => Promise, + transactionIdx: number, +): (...args: Args) => Promise { + const wrappedFn = async (...args: any[]) => { + const tx = args[transactionIdx]; + + if (!tx) { + try { + return await db.$transaction(async (trx) => { + args[transactionIdx] = trx; + const functionResult = await fn(...(args as Args)); + // The error has to be thrown in order to cancel the transaction + if (functionResult instanceof Err && functionResult.isErr()) throw functionResult.error; + + return functionResult; + }); + } catch (e) { + return err(e); + } + } else { + return (await fn(...(args as Args))) as Ret; + } + }; + + return wrappedFn as (...args: Args) => Promise; +} diff --git a/src/management-system-v2/lib/data/engines.ts b/src/management-system-v2/lib/data/engines.ts index 3f55d6ad6..9a0810d77 100644 --- a/src/management-system-v2/lib/data/engines.ts +++ b/src/management-system-v2/lib/data/engines.ts @@ -10,7 +10,7 @@ import { deleteSpaceEngine as _deleteDbEngine, } from '@/lib/data/db/engines'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; -import { UserErrorType, userError } from '../user-error'; +import { UserErrorType, userError } from '../server-error-handling/user-error'; import { z } from 'zod'; import { enableUseDB } from 'FeatureFlags'; @@ -100,7 +100,7 @@ export async function deleteSpaceEngine(engineId: string, environmentId: string const systemAdmin = (await getCurrentUser()).systemAdmin; try { - return await _deleteDbEngine(engineId, environmentId ?? null, ability, systemAdmin); + const result = await _deleteDbEngine(engineId, environmentId ?? null, ability, systemAdmin); } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); diff --git a/src/management-system-v2/lib/data/environment-memberships.ts b/src/management-system-v2/lib/data/environment-memberships.ts index 12aa549ef..498c1e694 100644 --- a/src/management-system-v2/lib/data/environment-memberships.ts +++ b/src/management-system-v2/lib/data/environment-memberships.ts @@ -1,7 +1,7 @@ 'use server'; import { getCurrentEnvironment } from '@/components/auth'; -import { UserErrorType, getErrorMessage, userError } from '../user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { z } from 'zod'; import { sendEmail } from '../email/mailer'; import renderOrganizationInviteEmail from '../organization-invite-email'; diff --git a/src/management-system-v2/lib/data/environments.ts b/src/management-system-v2/lib/data/environments.ts index 95a20898b..a7dbffdc3 100644 --- a/src/management-system-v2/lib/data/environments.ts +++ b/src/management-system-v2/lib/data/environments.ts @@ -5,7 +5,7 @@ import { UserOrganizationEnvironmentInput, UserOrganizationEnvironmentInputSchema, } from './environment-schema'; -import { UserErrorType, getErrorMessage, userError } from '../user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { UnauthorizedError } from '../ability/abilityHelper'; import { addEnvironment, @@ -31,12 +31,20 @@ export async function addOrganizationEnvironment( try { const environmentData = UserOrganizationEnvironmentInputSchema.parse(environmentInput); - return await addEnvironment({ + const result = await addEnvironment({ ownerId: userId, isActive: true, isOrganization: true, ...environmentData, }); + + if (result.isOk()) { + result.value; + } + if (result.isErr()) { + // Handle error + result.error; + } } catch (e) { console.error(e); return userError('Error adding environment'); diff --git a/src/management-system-v2/lib/data/file-manager-facade.ts b/src/management-system-v2/lib/data/file-manager-facade.ts index 62460fb99..e169a3f6c 100644 --- a/src/management-system-v2/lib/data/file-manager-facade.ts +++ b/src/management-system-v2/lib/data/file-manager-facade.ts @@ -17,7 +17,7 @@ import { use } from 'react'; import { checkValidity } from './processes'; import { env } from '@/lib/ms-config/env-vars'; import { getUsedImagesFromJson } from '@/components/html-form-editor/serialized-format-utils'; -import { getErrorMessage, userError } from '../user-error'; +import { getErrorMessage, userError } from '../server-error-handling/user-error'; const DEPLOYMENT_ENV = env.PROCEED_PUBLIC_STORAGE_DEPLOYMENT_ENV; diff --git a/src/management-system-v2/lib/data/folders.ts b/src/management-system-v2/lib/data/folders.ts index 5c9047773..9ed8aca80 100644 --- a/src/management-system-v2/lib/data/folders.ts +++ b/src/management-system-v2/lib/data/folders.ts @@ -2,7 +2,7 @@ import * as util from 'util'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { FolderUserInput, FolderUserInputSchema } from './folder-schema'; -import { UserErrorType, userError } from '../user-error'; +import { UserErrorType, userError } from '../server-error-handling/user-error'; import { TreeMap, toCaslResource } from '../ability/caslAbility'; import Ability, { UnauthorizedError } from '../ability/abilityHelper'; diff --git a/src/management-system-v2/lib/data/html-forms.ts b/src/management-system-v2/lib/data/html-forms.ts index bab2dd4ba..b2f3eccb8 100644 --- a/src/management-system-v2/lib/data/html-forms.ts +++ b/src/management-system-v2/lib/data/html-forms.ts @@ -1,7 +1,7 @@ 'use server'; import { HtmlForm } from '../html-form-schema'; -import { UserFacingError, getErrorMessage, userError } from '../user-error'; +import { UserFacingError, getErrorMessage, userError } from '../server-error-handling/user-error'; import { getHtmlForms as _getHtmlForms, getHtmlForm as _getHtmlForm, diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index 56c5dad64..ab9668939 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -17,7 +17,7 @@ import { updateBpmnCreatorAttributes, } from '@proceed/bpmn-helper'; import { createProcess, getFinalBpmn, updateFileNames } from '../helpers/processHelpers'; -import { UserErrorType, getErrorMessage, userError } from '../user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { areVersionsEqual, getLocalVersionBpmn, diff --git a/src/management-system-v2/lib/data/role-mappings.ts b/src/management-system-v2/lib/data/role-mappings.ts index 266cc6786..3466e4a94 100644 --- a/src/management-system-v2/lib/data/role-mappings.ts +++ b/src/management-system-v2/lib/data/role-mappings.ts @@ -6,7 +6,7 @@ import { addRoleMappings as _addRoleMappings, RoleMappingInput, } from '@/lib/data/db/iam/role-mappings'; -import { getErrorMessage, userError } from '../user-error'; +import { getErrorMessage, userError } from '../server-error-handling/user-error'; export async function addRoleMappings( environmentId: string, diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index 13585a807..ec04f34a7 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -2,7 +2,7 @@ import { getCurrentEnvironment } from '@/components/auth'; import { redirect } from 'next/navigation'; -import { UserErrorType, userError } from '../user-error'; +import { UserErrorType, userError } from '../server-error-handling/user-error'; import { RedirectType } from 'next/dist/client/components/redirect'; import { deleteRole, diff --git a/src/management-system-v2/lib/data/space-settings.ts b/src/management-system-v2/lib/data/space-settings.ts index 9bc02c68c..7b4764385 100644 --- a/src/management-system-v2/lib/data/space-settings.ts +++ b/src/management-system-v2/lib/data/space-settings.ts @@ -2,7 +2,7 @@ import { SettingGroup } from '@/app/(dashboard)/[environmentId]/settings/type-util'; import { getCurrentEnvironment } from '@/components/auth'; -import { UserErrorType, getErrorMessage, userError } from '@/lib/user-error'; +import { UserErrorType, getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; import { Record } from '@prisma/client/runtime/library'; import { getSpaceSettingsValues as _getSpaceSettingsValues, diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index e1beb3719..d1fef27e7 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -10,7 +10,7 @@ import { deleteUserTask as _deleteUserTask, } from './db/user-tasks'; import { UnauthorizedError } from '../ability/abilityHelper'; -import { UserErrorType, userError } from '../user-error'; +import { UserErrorType, userError } from '../server-error-handling/user-error'; import { UserTaskInput } from '../user-task-schema'; export async function getUserTasks() { diff --git a/src/management-system-v2/lib/data/users.tsx b/src/management-system-v2/lib/data/users.tsx index 8bdd42d37..ee93aa6e9 100644 --- a/src/management-system-v2/lib/data/users.tsx +++ b/src/management-system-v2/lib/data/users.tsx @@ -1,7 +1,7 @@ 'use server'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; -import { UserErrorType, getErrorMessage, userError } from '../user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { AuthenticatedUserData, AuthenticatedUserDataSchema } from './user-schema'; import { ReactNode } from 'react'; import { OrganizationEnvironment } from './environment-schema'; diff --git a/src/management-system-v2/lib/email-verification-tokens/server-actions.ts b/src/management-system-v2/lib/email-verification-tokens/server-actions.ts index 0b3160620..3d8ae3836 100644 --- a/src/management-system-v2/lib/email-verification-tokens/server-actions.ts +++ b/src/management-system-v2/lib/email-verification-tokens/server-actions.ts @@ -1,7 +1,7 @@ 'use server'; import { z } from 'zod'; -import { userError } from '../user-error'; +import { userError } from '../server-error-handling/user-error'; import { createChangeEmailVerificationToken, getTokenHash, notExpired } from './utils'; import { getCurrentUser } from '@/components/auth'; import { diff --git a/src/management-system-v2/lib/engines/deployment.ts b/src/management-system-v2/lib/engines/deployment.ts index d1796d8d3..d2beb940e 100644 --- a/src/management-system-v2/lib/engines/deployment.ts +++ b/src/management-system-v2/lib/engines/deployment.ts @@ -15,7 +15,7 @@ import { prepareExport } from '../process-export/export-preparation'; import { Prettify } from '../typescript-utils'; import { engineRequest } from './endpoints/index'; import { asyncForEach } from '../helpers/javascriptHelpers'; -import { UserFacingError } from '../user-error'; +import { UserFacingError } from '../server-error-handling/user-error'; type ProcessesExportData = Prettify>>; diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index c73ea7c29..baf890f1d 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -1,6 +1,6 @@ 'use server'; -import { UserFacingError, getErrorMessage, userError } from '../user-error'; +import { UserFacingError, getErrorMessage, userError } from '../server-error-handling/user-error'; import { DeployedProcessInfo, deployProcess as _deployProcess, diff --git a/src/management-system-v2/lib/errors.ts b/src/management-system-v2/lib/errors.ts deleted file mode 100644 index 86c08b20f..000000000 --- a/src/management-system-v2/lib/errors.ts +++ /dev/null @@ -1,8 +0,0 @@ -import { UserFacingError } from './user-error'; - -export class SpaceNotFoundError extends UserFacingError { - static prefix = '404' as const; - constructor(message?: string) { - super(`${SpaceNotFoundError.prefix}: ${message || 'Space not found'}`); - } -} diff --git a/src/management-system-v2/lib/page-error-handling.tsx b/src/management-system-v2/lib/page-error-handling.tsx new file mode 100644 index 000000000..2439b829d --- /dev/null +++ b/src/management-system-v2/lib/page-error-handling.tsx @@ -0,0 +1,8 @@ +import { Err, type Result } from 'neverthrow'; +import { UserFacingError } from './server-error-handling/user-error'; +import { UnauthorizedError } from './ability/abilityHelper'; +import { Err } from './errors'; + +export function errorResponse( + result: Err, +) {} diff --git a/src/management-system-v2/lib/process-export/export-preparation.ts b/src/management-system-v2/lib/process-export/export-preparation.ts index f6ca4863a..1b7d7480e 100644 --- a/src/management-system-v2/lib/process-export/export-preparation.ts +++ b/src/management-system-v2/lib/process-export/export-preparation.ts @@ -31,7 +31,7 @@ import { is as bpmnIs } from 'bpmn-js/lib/util/ModelUtil'; import { ArrayEntryType, truthyFilter } from '../typescript-utils'; import { SerializedNode } from '@craftjs/core'; -import { UserError } from '../user-error'; +import { UserError } from '../server-error-handling/user-error'; /** * The options that can be used to select what should be exported diff --git a/src/management-system-v2/lib/result.ts b/src/management-system-v2/lib/result.ts new file mode 100644 index 000000000..cc6b8d5b4 --- /dev/null +++ b/src/management-system-v2/lib/result.ts @@ -0,0 +1,50 @@ +export type Result = Ok | Err; + +export class Ok { + readonly isOk = true; + readonly isErr = false; + + constructor(readonly value: T) {} + + map(fn: (value: T) => U): Result { + return new Ok(fn(this.value)); + } + + mapErr(_fn: (error: E) => F): Result { + return new Ok(this.value); + } + + andThen(fn: (value: T) => Result): Result { + return fn(this.value); + } + + match(pattern: { ok: (value: T) => U; err: (error: E) => U }): U { + return pattern.ok(this.value); + } +} + +export class Err { + readonly isOk = false; + readonly isErr = true; + + constructor(readonly error: E) {} + + map(_fn: (value: T) => U): Result { + return new Err(this.error); + } + + mapErr(fn: (error: E) => F): Result { + return new Err(fn(this.error)); + } + + andThen(_fn: (value: T) => Result): Result { + return new Err(this.error); + } + + match(pattern: { ok: (value: T) => U; err: (error: E) => U }): U { + return pattern.err(this.error); + } +} + +export const ok = (value: T): Result => new Ok(value); +export const err = (error: E): Result => new Err(error); diff --git a/src/management-system-v2/lib/server-error-handling/errors.ts b/src/management-system-v2/lib/server-error-handling/errors.ts new file mode 100644 index 000000000..e4ba76bf5 --- /dev/null +++ b/src/management-system-v2/lib/server-error-handling/errors.ts @@ -0,0 +1,12 @@ +import { UserFacingError } from './user-error'; + +export class NotFoundError extends UserFacingError { + constructor(message?: string) { + super(`${message || 'Not found'}`); + } +} +export class SpaceNotFoundError extends UserFacingError { + constructor(message?: string) { + super(`${message || 'Space not found'}`); + } +} diff --git a/src/management-system-v2/lib/server-error-handling/page-error-response.tsx b/src/management-system-v2/lib/server-error-handling/page-error-response.tsx new file mode 100644 index 000000000..d8ed5dfbd --- /dev/null +++ b/src/management-system-v2/lib/server-error-handling/page-error-response.tsx @@ -0,0 +1,32 @@ +import { Err } from 'neverthrow'; +import { UserFacingError } from './user-error'; +import Content from '@/components/content'; +import { NotFoundError, SpaceNotFoundError } from '@/lib/server-error-handling/errors'; +import { Result, ResultProps } from 'antd'; +import { UnauthorizedError } from '../ability/abilityHelper'; +import RetryButton from './retry-button'; + +export function errorResponse( + result: Err, +) { + const error = result.error; + + let title = 'Something Went wrong'; + let status: ResultProps['status'] = 'warning'; + + if (error instanceof UserFacingError) { + title = error.message; + if (error instanceof NotFoundError || error instanceof SpaceNotFoundError) { + status = '404'; + } + } else if (error instanceof UnauthorizedError) { + title = 'Not allowed'; + status = '403'; + } + + return ( + + } /> + + ); +} diff --git a/src/management-system-v2/lib/server-error-handling/retry-button.tsx b/src/management-system-v2/lib/server-error-handling/retry-button.tsx new file mode 100644 index 000000000..76cb73bba --- /dev/null +++ b/src/management-system-v2/lib/server-error-handling/retry-button.tsx @@ -0,0 +1,10 @@ +'use client'; + +import { Button, ButtonProps } from 'antd'; +import { useRouter } from 'next/navigation'; + +export default function RetryButton(props: ButtonProps) { + const router = useRouter(); + + return - {role?.name} + {role.value.name} } > diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/page.tsx index 43291d0c7..fdcea9cf2 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/page.tsx @@ -3,18 +3,26 @@ import Content from '@/components/content'; import { getRolesWithMembers } from '@/lib/data/db/iam/roles'; import RolesPage from './role-page'; import UnauthorizedFallback from '@/components/unauthorized-fallback'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const Page = async ({ params }: { params: { environmentId: string } }) => { - const { ability, activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; // if (!ability.can('manage', 'Role')) return ; if (!ability.can('admin', 'All')) return ; const roles = await getRolesWithMembers(activeEnvironment.spaceId, ability); + if (roles.isErr()) { + return errorResponse(roles); + } return ( - + ); }; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx index bd475af18..52e9d8cba 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx @@ -2,17 +2,36 @@ import { getCurrentEnvironment } from '@/components/auth'; import UsersPage from './users-page'; import Content from '@/components/content'; import UnauthorizedFallback from '@/components/unauthorized-fallback'; -import { getFullMembersWithRoles } from '@/lib/data/db/iam/memberships'; +import { getMembers } from '@/lib/data/db/iam/memberships'; +import { getUserById } from '@/lib/data/db/iam/users'; +import { AuthenticatedUser } from '@/lib/data/user-schema'; +import { asyncMap } from '@/lib/helpers/javascriptHelpers'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { Result } from 'neverthrow'; const Page = async ({ params }: { params: { environmentId: string } }) => { - const { ability, activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; if (!ability.can('manage', 'User')) return ; - const users = await getFullMembersWithRoles(activeEnvironment.spaceId, ability); + const memberships = await getMembers(activeEnvironment.spaceId, ability); + if (memberships.isErr()) { + return errorResponse(memberships); + } + + const users = Result.combine( + await asyncMap(memberships.value, (user) => getUserById(user.userId)), + ); + if (users.isErr()) { + return errorResponse(users); + } return ( - + ); }; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx index 6c2ae05ee..386c9ad1e 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/layout.tsx @@ -4,6 +4,7 @@ import { SetAbility } from '@/lib/abilityStore'; import Layout, { ExtendedMenuItems } from './layout-client'; import { getUserOrganizationEnvironments } from '@/lib/data/db/iam/memberships'; import { MenuProps } from 'antd'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; import { PartitionOutlined, @@ -42,38 +43,72 @@ import { customLinkIcons } from '@/lib/custom-links/icons'; import { CustomNavigationLink } from '@/lib/custom-links/custom-link'; import { env } from '@/lib/ms-config/env-vars'; import { getUserPassword } from '@/lib/data/db/iam/users'; +import { Result } from 'neverthrow'; const DashboardLayout = async ({ children, params, }: PropsWithChildren<{ params: { environmentId: string } }>) => { - const { userId, systemAdmin, user } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { userId, systemAdmin, user } = currentUser.value; + + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { activeEnvironment, ability } = currentSpace.value; - const { activeEnvironment, ability } = await getCurrentEnvironment(params.environmentId); const can = ability.can.bind(ability); const userEnvironments: Environment[] = []; - if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) - userEnvironments.push(await getEnvironmentById(userId))!; + if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { + const personalEnvironment = await getEnvironmentById(userId); + if (personalEnvironment.isErr()) return errorResponse(personalEnvironment); + + userEnvironments.push(personalEnvironment.value); + } const userOrgEnvs = await getUserOrganizationEnvironments(userId); - const orgEnvironments = await asyncMap( - userOrgEnvs, - async (envId) => (await getEnvironmentById(envId))!, + if (userOrgEnvs.isErr()) { + return errorResponse(userOrgEnvs); + } + + const orgEnvironments = Result.combine( + await asyncMap(userOrgEnvs.value, async (envId) => await getEnvironmentById(envId)), ); + if (orgEnvironments.isErr()) { + return errorResponse(orgEnvironments); + } + const msConfig = await getMSConfig(); - userEnvironments.push(...orgEnvironments); + userEnvironments.push(...orgEnvironments.value); - const userRules = systemAdmin - ? getSystemAdminRules(activeEnvironment.isOrganization) - : await getUserRules(userId, activeEnvironment.spaceId); + let userRules; + if (systemAdmin) { + userRules = getSystemAdminRules(activeEnvironment.isOrganization); + } else { + const rules = await getUserRules(userId, activeEnvironment.spaceId); + if (rules.isErr()) { + return errorResponse(rules); + } + + userRules = rules.value; + } const generalSettings = await getSpaceSettingsValues( activeEnvironment.spaceId, 'general-settings', ); - const customNavLinks: CustomNavigationLink[] = generalSettings.customNavigationLinks?.links || []; + if (generalSettings.isErr()) { + return errorResponse(generalSettings); + } + + const customNavLinks: CustomNavigationLink[] = + generalSettings.value.customNavigationLinks?.links || []; const topCustomNavLinks = customNavLinks.filter((link) => link.position === 'top'); const middleCustomNavLinks = customNavLinks.filter((link) => link.position === 'middle'); const bottomCustomNavLinks = customNavLinks.filter((link) => link.position === 'bottom'); @@ -112,7 +147,11 @@ const DashboardLayout = async ({ } const userPassword = await getUserPassword(user!.id); - const userNeedsToChangePassword = userPassword ? userPassword.isTemporaryPassword : false; + if (userPassword.isErr()) { + return errorResponse(userPassword); + } + + const userNeedsToChangePassword = userPassword ? userPassword.value?.isTemporaryPassword : false; let layoutMenuItems: ExtendedMenuItems = []; @@ -136,17 +175,20 @@ const DashboardLayout = async ({ activeEnvironment.spaceId, 'process-documentation', ); + if (documentationSettings.isErr()) { + return errorResponse(documentationSettings); + } - if (documentationSettings.active !== false) { + if (documentationSettings.value.active !== false) { const processRegex = '/processes($|/)'; let children: ExtendedMenuItems = [ - documentationSettings.list?.active !== false && { + documentationSettings.value.list?.active !== false && { key: 'processes-list', label: List, icon: , selectedRegex: '/processes/list($|/)', }, - documentationSettings.editor?.active !== false && { + documentationSettings.value.editor?.active !== false && { key: 'processes-editor', label: Editor, icon: , @@ -170,12 +212,18 @@ const DashboardLayout = async ({ activeEnvironment.spaceId, 'process-automation', ); + if (automationSettings.isErr()) { + return errorResponse(automationSettings); + } - if (msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE && automationSettings.active !== false) { + if ( + msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE && + automationSettings.value.active !== false + ) { let childRegex = ''; let children: ExtendedMenuItems = []; - if (automationSettings.task_editor?.active !== false) { + if (automationSettings.value.task_editor?.active !== false) { childRegex = '/tasks($|/)'; children.push({ key: 'task-editor', @@ -200,11 +248,11 @@ const DashboardLayout = async ({ } if (msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) { - if (automationSettings.active !== false) { + if (automationSettings.value.active !== false) { let childRegex = ''; let children: ExtendedMenuItems = []; - if (automationSettings.dashboard?.active !== false) { + if (automationSettings.value.dashboard?.active !== false) { const dashboardRegex = '/executions-dashboard($|/)'; childRegex = !childRegex ? dashboardRegex : `(${childRegex})|(${dashboardRegex})`; children.push({ @@ -214,7 +262,7 @@ const DashboardLayout = async ({ selectedRegex: dashboardRegex, }); } - if (automationSettings.executions?.active !== false) { + if (automationSettings.value.executions?.active !== false) { const executionsRegex = '/executions($|/)'; childRegex = !childRegex ? executionsRegex : `(${childRegex})|(${executionsRegex})`; children.push({ @@ -224,7 +272,7 @@ const DashboardLayout = async ({ selectedRegex: executionsRegex, }); } - if (automationSettings.machines?.active !== false) { + if (automationSettings.value.machines?.active !== false) { const machinesRegex = '/engines($|/)'; childRegex = !childRegex ? machinesRegex : `(${childRegex})|(${machinesRegex})`; children.push({ @@ -408,14 +456,22 @@ const DashboardLayout = async ({ ); } - const logo = (await getSpaceLogo(activeEnvironment.spaceId))?.spaceLogo ?? undefined; + const spaceLogo = await getSpaceLogo(activeEnvironment.spaceId); + if (spaceLogo.isErr()) { + return errorResponse(spaceLogo); + } + + const logo = spaceLogo.value?.spaceLogo ?? undefined; + + const treeMap = await getSpaceFolderTree(activeEnvironment.spaceId); + if (treeMap.isErr()) return errorResponse(treeMap); return ( <> { - const { ability, activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; if ( !activeEnvironment.isOrganization || (!ability.can('update', 'Environment') && !ability.can('delete', 'Environment')) @@ -16,9 +21,12 @@ const GeneralSettingsPage = async ({ params }: { params: { environmentId: string throw new UnauthorizedError(); } - const organization = (await getEnvironmentById( - activeEnvironment.spaceId, - )) as OrganizationEnvironment; + const _organization = await getEnvironmentById(activeEnvironment.spaceId); + if (_organization.isErr()) { + return errorResponse(_organization); + } + + const organization = _organization.value as OrganizationEnvironment; const children: (Setting | SettingGroup)[] = []; if (ability.can('update', 'Environment')) { diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx index 3b630fe1e..c6b841a63 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx @@ -13,6 +13,9 @@ import { RoleType, UserType } from './use-potentialOwner-store'; import type { Process } from '@/lib/data/process-schema'; import { redirect } from 'next/navigation'; import { spaceURL } from '@/lib/utils'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; +import { err } from 'neverthrow'; type ProcessPageProps = { params: { processId: string; environmentId: string; mode: string }; @@ -32,17 +35,24 @@ const ProcessComponent = async ({ // refresh in processes.tsx anymore? //console.log('processId', processId); //console.log('query', searchParams); - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentSpace = await getCurrentEnvironment(environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; const selectedVersionId = searchParams.version; // Only load BPMN if no version selected (for latest version) const process = await getProcess(processId, !selectedVersionId); + if (process.isErr()) { + return errorResponse(process); + } // For list view: check for redirect if (isListView) { // If no version specified but released versions exist, redirect to last released version - if (!searchParams.version && process.versions.length > 0) { - const lastVersionId = process.versions[process.versions.length - 1].id; + if (!searchParams.version && process.value.versions.length > 0) { + const lastVersionId = process.value.versions[process.value.versions.length - 1].id; const currentPath = `/processes/list/${processId}`; const redirectUrl = spaceURL(activeEnvironment, `${currentPath}?version=${lastVersionId}`); redirect(redirectUrl); @@ -66,15 +76,25 @@ const ProcessComponent = async ({ // return acc; // }, {} as UserType); - if (!ability.can('view', toCaslResource('Process', process))) { + if (!ability.can('view', toCaslResource('Process', process.value))) { throw new UnauthorizedError(); } - const selectedVersionBpmn = selectedVersionId - ? await getProcessBPMN(processId, environmentId, selectedVersionId) - : process.bpmn; + let selectedVersionBpmn; + if (selectedVersionId) { + const bpmn = await getProcessBPMN(processId, environmentId, selectedVersionId); + // TODO: don't use server action + if (isUserErrorResponse(bpmn)) { + return errorResponse(err()); + } + + selectedVersionBpmn = bpmn; + } else { + selectedVersionBpmn = process.value.bpmn; + } + const selectedVersion = selectedVersionId - ? process.versions.find((version) => version.id === selectedVersionId) + ? process.value.versions.find((version) => version.id === selectedVersionId) : undefined; // Since the user is able to minimize and close the page, everything is in a @@ -82,8 +102,8 @@ const ProcessComponent = async ({ return ( <> } timelineComponent={ } /> diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/potentialOwner-server-action.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/potentialOwner-server-action.ts index 8639bc654..e9f381cdf 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/potentialOwner-server-action.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/potentialOwner-server-action.ts @@ -3,17 +3,26 @@ import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { getRolesWithMembers } from '@/lib/data/db/iam/roles'; import { RoleType, UserType } from './use-potentialOwner-store'; +import { getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; export const fetchPotentialOwner = async (environmentId: string) => { const user: UserType = {}; const roles: RoleType = {}; if (environmentId) { - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentSpace = await getCurrentEnvironment(environmentId); + if (currentSpace.isErr()) { + return userError(getErrorMessage(currentSpace.error)); + } + + const { ability, activeEnvironment } = currentSpace.value; if (activeEnvironment.isOrganization) { const rawRoles = await getRolesWithMembers(activeEnvironment.spaceId, ability); + if (rawRoles.isErr()) { + return userError(getErrorMessage(rawRoles.error)); + } - rawRoles.forEach((role) => { + rawRoles.value.forEach((role) => { roles[role.id] = role.name; role.members.forEach((member) => { @@ -26,8 +35,12 @@ export const fetchPotentialOwner = async (environmentId: string) => { } else { // make sure to get the current user that might not be assigned to any role const u = await getCurrentUser(); - if (u.session?.user) { - const currUser = u.session.user; + if (u.isErr()) { + return userError(getErrorMessage(u.error)); + } + + if (u.value.session?.user) { + const currUser = u.value.session.user; if (!currUser.isGuest) { user[currUser.id] = { userName: currUser.username, diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-potentialOwner-store.ts b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-potentialOwner-store.ts index 220baf4c5..0722f8435 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-potentialOwner-store.ts +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-potentialOwner-store.ts @@ -3,6 +3,7 @@ import { immer } from 'zustand/middleware/immer'; import { useEffect } from 'react'; import { fetchPotentialOwner } from './potentialOwner-server-action'; import { useEnvironment } from '@/components/auth-can'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; export type UserType = { [key: string]: { @@ -50,7 +51,10 @@ export const useInitialisePotentialOwnerStore = () => { const environment = useEnvironment(); useEffect(() => { const initialiseStore = async () => { - const { user, roles } = await fetchPotentialOwner(environment.spaceId); + const response = await fetchPotentialOwner(environment.spaceId); + if (isUserErrorResponse(response)) return; + + const { user, roles } = response; const store = usePotentialOwnerStore.getState(); store.setUser(user); store.setRoles(roles); diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/folder/[folderId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/folder/[folderId]/page.tsx index 093ce90e8..afe45468a 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/folder/[folderId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/folder/[folderId]/page.tsx @@ -16,6 +16,7 @@ import EllipsisBreadcrumb from '@/components/ellipsis-breadcrumb'; import { ComponentProps } from 'react'; import { spaceURL } from '@/lib/utils'; import { getFolderById, getRootFolder, getFolderContents } from '@/lib/data/db/folders'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; export type ListItem = ProcessMetadata | (Folder & { type: 'folder' }); const ProcessesPage = async ({ @@ -23,25 +24,30 @@ const ProcessesPage = async ({ }: { params: { environmentId: string; mode: string; folderId?: string }; }) => { - const { ability, activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) return errorResponse(currentSpace); + const { ability, activeEnvironment } = currentSpace.value; const favs = await getUsersFavourites(); const rootFolder = await getRootFolder(activeEnvironment.spaceId, ability); + if (rootFolder.isErr()) return errorResponse(rootFolder); const folder = await getFolderById( - params.folderId ? decodeURIComponent(params.folderId) : rootFolder.id, + params.folderId ? decodeURIComponent(params.folderId) : rootFolder.value.id, ); + if (folder.isErr()) return errorResponse(folder); - const folderContents = await getFolderContents(folder.id, ability); + const folderContents = await getFolderContents(folder.value.id, ability); + if (folderContents.isErr()) return errorResponse(folderContents); const isListView = params.mode === 'list'; const folderContentsFiltered = isListView - ? folderContents.filter( + ? folderContents.value.filter( (folderContent) => folderContent.type === 'folder' || folderContent.versions.length > 0, ) - : folderContents; + : folderContents.value; const hasNoReleasedProcesses = isListView ? folderContentsFiltered.every((item) => item.type === 'folder') @@ -49,7 +55,7 @@ const ProcessesPage = async ({ const pathToFolder: ComponentProps['items'] = []; const wrappingFolderIds = [] as string[]; - let currentFolder: Folder | null = folder; + let currentFolder: Folder | null = folder.value; do { pathToFolder.push({ title: ( @@ -61,7 +67,17 @@ const ProcessesPage = async ({ ), }); if (currentFolder) wrappingFolderIds.push(currentFolder.id); - currentFolder = currentFolder.parentId ? await getFolderById(currentFolder.parentId) : null; + + if (currentFolder.parentId) { + const result = await getFolderById(currentFolder.parentId); + if (result.isErr()) { + return errorResponse(result); + } + + currentFolder = result.value; + } else { + currentFolder = null; + } } while (currentFolder); pathToFolder.reverse(); wrappingFolderIds.reverse(); @@ -71,11 +87,11 @@ const ProcessesPage = async ({ - {folder.parentId && ( + {folder.value.parentId && ( - {`${getUserName(user as User)}'s Spaces`} + {`${getUserName(user.value as User)}'s Spaces`} ); - const userSpaces: any[] = [await getEnvironmentById(userId)]; + const personalEnvironment = await getEnvironmentById(userId); + if (personalEnvironment.isErr()) return errorResponse(personalEnvironment); + const userSpaces: any[] = [personalEnvironment.value]; + const userOrgEnvs = await getUserOrganizationEnvironments(userId); - const orgEnvironmentsPromises = userOrgEnvs.map(async (environmentId) => { + if (userOrgEnvs.isErr()) return errorResponse(userOrgEnvs); + const orgEnvironmentsPromises = userOrgEnvs.value.map(async (environmentId) => { return await getEnvironmentById(environmentId); }); - const orgEnvironments = await Promise.all(orgEnvironmentsPromises); + const orgEnvironments = Result.combine(await Promise.all(orgEnvironmentsPromises)); + if (orgEnvironments.isErr()) { + return errorResponse(orgEnvironments); + } - userSpaces.push(...orgEnvironments); + userSpaces.push(...orgEnvironments.value); spacesTableRepresentation = await getSpaceRepresentation(userSpaces); } else { - spacesTableRepresentation = await getSpaceRepresentation( - (await getEnvironments()) as Environment[], - ); + const environments = await getEnvironments(); + if (environments.isErr()) { + return errorResponse(environments); + } + + spacesTableRepresentation = await getSpaceRepresentation(environments.value as Environment[]); } + if (spacesTableRepresentation.isErr()) return errorResponse(spacesTableRepresentation); + return ( - + ); } diff --git a/src/management-system-v2/app/admin/spaces/space-representation.ts b/src/management-system-v2/app/admin/spaces/space-representation.ts index a5fb194de..4ffc27942 100644 --- a/src/management-system-v2/app/admin/spaces/space-representation.ts +++ b/src/management-system-v2/app/admin/spaces/space-representation.ts @@ -2,6 +2,7 @@ import 'server-only'; import { Environment } from '@/lib/data/environment-schema'; import { getUserById } from '@/lib/data/db/iam/users'; import { User } from '@/lib/data/user-schema'; +import { Result, err, ok } from 'neverthrow'; export function getUserName(user: User) { if (user.isGuest) return 'Guest'; @@ -12,35 +13,41 @@ export function getUserName(user: User) { } export type SpaceRepresentation = { id: string; name: string; type: string; owner: string }; -export function getSpaceRepresentation(spaces: Environment[]): Promise { - return Promise.all( - spaces.map(async (space) => { - if (space.isOrganization && !space.isActive) - return { - id: space.id, - name: `${space.name}`, - type: 'Organization', - owner: 'None', - }; +export async function getSpaceRepresentation(spaces: Environment[]) { + return Result.combine( + await Promise.all( + spaces.map(async (space) => { + if (space.isOrganization && !space.isActive) { + return ok({ + id: space.id, + name: `${space.name}`, + type: 'Organization', + owner: 'None', + }); + } + + const user = await getUserById(space.isOrganization ? space.ownerId : space.id); + if (user.isErr()) return user; + if (!user.value) err(new Error('Space user not found')); - const user = await getUserById(space.isOrganization ? space.ownerId : space.id); - if (!user) throw new Error('Space user not found'); - const userName = getUserName(user as User); + const userName = getUserName(user.value as User); - if (space.isOrganization) - return { + if (space.isOrganization) { + return ok({ + id: space.id, + name: `${space.name}`, + type: 'Organization', + owner: userName, + }); + } + + return ok({ id: space.id, - name: `${space.name}`, - type: 'Organization', + name: `Personal space: ${userName}`, + type: 'Personal space', owner: userName, - }; - - return { - id: space.id, - name: `Personal space: ${userName}`, - type: 'Personal space', - owner: userName, - }; - }), + }); + }), + ), ); } diff --git a/src/management-system-v2/app/admin/systemadmins/page.tsx b/src/management-system-v2/app/admin/systemadmins/page.tsx index 4117609bc..09daf7762 100644 --- a/src/management-system-v2/app/admin/systemadmins/page.tsx +++ b/src/management-system-v2/app/admin/systemadmins/page.tsx @@ -9,24 +9,33 @@ import { } from '@/lib/data/db/iam/system-admins'; import { getUserById, getUsers } from '@/lib/data/db/iam/users'; import { AuthenticatedUser } from '@/lib/data/user-schema'; -import { UserErrorType, userError } from '@/lib/server-error-handling/user-error'; +import { UserErrorType, getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; import { notFound, redirect } from 'next/navigation'; import SystemAdminsTable from './admins-table'; import { SystemAdminCreationInput } from '@/lib/data/system-admin-schema'; import { getMSConfig } from '@/lib/ms-config/ms-config'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { Result, ok } from 'neverthrow'; async function deleteAdmins(userIds: string[]) { 'use server'; - const { systemAdmin } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { systemAdmin } = currentUser.value; if (!systemAdmin || systemAdmin.role !== 'admin') return userError('Not a system admin', UserErrorType.PermissionError); try { for (const userId of userIds) { const adminMapping = await getSystemAdminByUserId(userId); - if (!adminMapping) return userError('Admin not found'); + if (adminMapping.isErr()) { + return userError(getErrorMessage(adminMapping.error)); + } + if (!adminMapping.value) return userError('Admin not found'); - deleteSystemAdmin(adminMapping.id); + deleteSystemAdmin(adminMapping.value.id); } } catch (e) { return userError('Something went wrong'); @@ -36,7 +45,11 @@ export type deleteAdmins = typeof deleteAdmins; async function addAdmin(admins: SystemAdminCreationInput[]) { 'use server'; - const { systemAdmin } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { systemAdmin } = currentUser.value; if (!systemAdmin || systemAdmin.role !== 'admin') return userError('Not a system admin', UserErrorType.PermissionError); @@ -52,16 +65,28 @@ export type addAdmin = typeof addAdmin; async function getNonAdminUsers(page: number = 1, pageSize: number = 10) { 'use server'; - const { systemAdmin } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + + const { systemAdmin } = currentUser.value; if (!systemAdmin || systemAdmin.role !== 'admin') return userError('Not a system admin', UserErrorType.PermissionError); try { const systemAdmins = await getSystemAdmins(); - const { users, pagination } = await getUsers(page, pageSize); + if (systemAdmins.isErr()) { + return userError(getErrorMessage(systemAdmins.error)); + } + + const usersResult = await getUsers(page, pageSize); + if (usersResult.isErr()) return userError(getErrorMessage(usersResult.error)); + + const { users, pagination } = usersResult.value; const filteredUsers = users.filter( - (user) => !user.isGuest && !systemAdmins.some((admin) => admin.userId === user.id), + (user) => !user.isGuest && !systemAdmins.value.some((admin) => admin.userId === user.id), ) as AuthenticatedUser[]; const totalFilteredUsers = filteredUsers.length; @@ -86,27 +111,44 @@ export default async function ManageAdminsPage() { if (!msConfig.PROCEED_PUBLIC_IAM_ACTIVE) return notFound(); const user = await getCurrentUser(); - if (!user.session) redirect('/'); - const adminData = await getSystemAdminByUserId(user.userId); - if (!adminData) redirect('/'); - if (adminData.role !== 'admin') return ; + if (user.isErr()) return errorResponse(user); + if (!user.value.session) redirect('/'); - const getFullSystemAdmins = async (): Promise<(AuthenticatedUser & { role: 'admin' })[]> => { + const adminData = await getSystemAdminByUserId(user.value.userId); + if (adminData.isErr()) { + return errorResponse(adminData); + } + if (!adminData.value) redirect('/'); + if (adminData.value.role !== 'admin') return ; + + type aa = Promise<(AuthenticatedUser & { role: 'admin' })[]>; + const getFullSystemAdmins = async () => { const admins = await getSystemAdmins(); - return Promise.all( - admins.map(async (admin) => { - const user = (await getUserById(admin.userId)) as AuthenticatedUser; - return { ...user, role: admin.role }; - }), + if (admins.isErr()) return admins; + + return Result.combine( + await Promise.all( + admins.value.map(async (admin) => { + const user = await getUserById(admin.userId); + if (user.isErr()) { + return user; + } + + return ok({ ...(user.value as AuthenticatedUser), role: admin.role }); + }), + ), ); }; const adminsList = await getFullSystemAdmins(); + if (adminsList.isErr()) { + return errorResponse(adminsList); + } return ( { - const orgs = (await getUserOrganizationEnvironments(user.id)).length; - if (orgs > 0) { - console.log(await getUserOrganizationEnvironments(user.id)); - } - return user.isGuest - ? { - ...user, - isGuest: false as const, - email: '', - username: 'Guest', - firstName: 'Guest', - lastName: '', - orgs, - } - : { ...user, orgs }; - }), + const processedUsers = Result.combine( + await Promise.all( + paginatedUsers.map(async (user) => { + const userOrgs = await getUserOrganizationEnvironments(user.id); + if (userOrgs.isErr()) return userOrgs as Err; + + const orgs = userOrgs.value.length; + const ret = user.isGuest + ? { + ...user, + isGuest: false as const, + email: '', + username: 'Guest', + firstName: 'Guest', + lastName: '', + orgs, + } + : { ...user, orgs }; + + return ok(ret); + }), + ), ); + if (processedUsers.isErr()) { + return processedUsers; + } - return { - users: processedUsers, + return ok({ + users: processedUsers.value, pagination, - }; + }); } - const { users, pagination } = await getProcessedUsers(); + const proceedUsers = await getProcessedUsers(); + if (proceedUsers.isErr()) return errorResponse(proceedUsers); + + const { users } = proceedUsers.value; return ( diff --git a/src/management-system-v2/app/api/private/[environmentId]/logo/route.ts b/src/management-system-v2/app/api/private/[environmentId]/logo/route.ts deleted file mode 100644 index 81029deec..000000000 --- a/src/management-system-v2/app/api/private/[environmentId]/logo/route.ts +++ /dev/null @@ -1,122 +0,0 @@ -import { getCurrentEnvironment } from '@/components/auth'; -import { NextRequest, NextResponse } from 'next/server'; -import { invalidRequest, readImage } from '../image-helpers'; -import { deleteSpaceLogo, getEnvironmentById, getSpaceLogo } from '@/lib/data/db/iam/environments'; - -export async function GET( - _: NextRequest, - { params: { environmentId } }: { params: { environmentId: string } }, -) { - const organization = await getEnvironmentById(environmentId); - if (!organization) - return new NextResponse(null, { - status: 404, - statusText: 'Space with this id does not exist.', - }); - if (!organization.isOrganization) - return new NextResponse(null, { - status: 405, - statusText: "Personal spaces don't support logos", - }); - - try { - await getCurrentEnvironment(environmentId, { - permissionErrorHandling: { - action: 'throw-error', - }, - }); - } catch (e) { - return new NextResponse(null, { - status: 403, - statusText: "You're not allowed to view this logo", - }); - } - - // TODO: implement this here - // const imageBuffer = (await getOrganizationLogo(decodeURIComponent(environmentId)))?.logo; - // - // if (!imageBuffer) - // return new NextResponse(null, { - // status: 204, - // statusText: 'Organization has no logo', - // }); - // - // const fileType = await fileTypeFromBuffer(imageBuffer); - // - // if (!fileType) { - // return new NextResponse(null, { - // status: 415, - // statusText: 'Can not read file type of requested image', - // }); - // } - // - // const headers = new Headers(); - // headers.set('Content-Type', fileType.mime); - // - // return new NextResponse(imageBuffer, { status: 200, statusText: 'OK', headers }); -} - -async function updateOrgLogo( - request: NextRequest, - { params: { environmentId } }: { params: { environmentId: string } }, -) { - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); - - if (!activeEnvironment.isOrganization) - return new NextResponse(null, { - status: 405, - statusText: "Personal spaces don't support logos", - }); - - if (!ability.can('update', 'Environment', { environmentId: activeEnvironment.spaceId })) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to change the logo from an organization', - }); - } - - const isInvalidRequest = invalidRequest(request); - if (isInvalidRequest) return isInvalidRequest; - - const readImageResult = await readImage(request); - if (readImageResult.error) return readImageResult.error; - - // TODO: implement this here - // saveEntityFile(EntityType.ORGANIZATION, - - return new NextResponse(activeEnvironment.spaceId, { status: 201, statusText: 'Created' }); -} - -export { updateOrgLogo as POST, updateOrgLogo as PUT }; - -export async function DELETE( - _: NextRequest, - { params: { environmentId } }: { params: { environmentId: string } }, -) { - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); - - if (!activeEnvironment.isOrganization) - return new NextResponse(null, { - status: 405, - statusText: "Personal spaces don't support logos", - }); - - if (!ability.can('update', 'Environment', { environmentId: activeEnvironment.spaceId })) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to change the logo from an organization', - }); - } - - try { - deleteSpaceLogo(activeEnvironment.spaceId); - } catch (e) { - // We assume the organization didn't have a logo - return new NextResponse(null, { - status: 405, - statusText: 'Something went wrong', - }); - } - - return new NextResponse(activeEnvironment.spaceId, { status: 200, statusText: 'Created' }); -} diff --git a/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/[imageFileName]/route.ts b/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/[imageFileName]/route.ts index 34b24d498..ee244c59f 100644 --- a/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/[imageFileName]/route.ts +++ b/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/[imageFileName]/route.ts @@ -13,6 +13,7 @@ import jwt from 'jsonwebtoken'; import { TokenPayload } from '@/lib/sharing/process-sharing'; import { invalidRequest, readImage } from '../../../../image-helpers'; import { v4 } from 'uuid'; +import { getErrorMessage } from '@/lib/server-error-handling/user-error'; export async function GET( request: NextRequest, @@ -20,60 +21,73 @@ export async function GET( params: { environmentId, processId, imageFileName }, }: { params: { environmentId: string; processId: string; imageFileName: string } }, ) { - const processMeta = await getProcess(processId, false); - - if (!processMeta) { - return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', - }); - } - - let canAccess = false; - - // if the user is not unauthenticated check if they have access to the process due to being an owner - if (environmentId !== 'unauthenticated') { - const { ability } = await getCurrentEnvironment(environmentId); - - canAccess = ability.can('view', toCaslResource('Process', processMeta)); - } - - // if the user is not an owner check if they have access if a share token is provided in the query data of the url - const shareToken = request.nextUrl.searchParams.get('shareToken'); - if (!canAccess && shareToken) { - const key = process.env.SHARING_ENCRYPTION_SECRET!; - const { - processId: shareProcessId, - embeddedMode, - timestamp, - } = jwt.verify(shareToken, key!) as TokenPayload; - - canAccess = - !embeddedMode && shareProcessId === processId && timestamp === processMeta.shareTimestamp; - } - - if (!canAccess) { + try { + const processMeta = await getProcess(processId, false); + if (processMeta.isErr()) throw processMeta.error; + + if (!processMeta.value) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } + + let canAccess = false; + + // if the user is not unauthenticated check if they have access to the process due to being an owner + if (environmentId !== 'unauthenticated') { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; + const { ability } = currentEnvironment.value; + + canAccess = ability.can('view', toCaslResource('Process', processMeta.value)); + } + + // if the user is not an owner check if they have access if a share token is provided in the query data of the url + const shareToken = request.nextUrl.searchParams.get('shareToken'); + if (!canAccess && shareToken) { + const key = process.env.SHARING_ENCRYPTION_SECRET!; + const { + processId: shareProcessId, + embeddedMode, + timestamp, + } = jwt.verify(shareToken, key!) as TokenPayload; + + canAccess = + !embeddedMode && + shareProcessId === processId && + timestamp === processMeta.value.shareTimestamp; + } + + if (!canAccess) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to view image in this process', + }); + } + + const imageBuffer = await getProcessImage(processId, imageFileName); + if (imageBuffer.isErr()) throw imageBuffer.error; + + const fileType = await fileTypeFromBuffer(imageBuffer.value); + + if (!fileType) { + return new NextResponse(null, { + status: 415, + statusText: 'Can not read file type of requested image', + }); + } + + const headers = new Headers(); + headers.set('Content-Type', fileType.mime); + + return new NextResponse(imageBuffer.value, { status: 200, statusText: 'OK', headers }); + } catch (error) { return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to view image in this process', + status: 500, + statusText: getErrorMessage(error), }); } - - const imageBuffer = await getProcessImage(processId, imageFileName); - - const fileType = await fileTypeFromBuffer(imageBuffer); - - if (!fileType) { - return new NextResponse(null, { - status: 415, - statusText: 'Can not read file type of requested image', - }); - } - - const headers = new Headers(); - headers.set('Content-Type', fileType.mime); - - return new NextResponse(imageBuffer, { status: 200, statusText: 'OK', headers }); } export async function PUT( @@ -82,35 +96,45 @@ export async function PUT( params: { environmentId, processId, imageFileName }, }: { params: { environmentId: string; processId: string; imageFileName: string } }, ) { - const { ability } = await getCurrentEnvironment(environmentId); + try { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; + const { ability } = currentEnvironment.value; - const process = await getProcess(processId, false); + const process = await getProcess(processId, false); + if (process.isErr()) throw process.error; - if (!process) { - return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', - }); - } + if (!process.value) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } - if (!ability.can('view', toCaslResource('Process', process))) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to view image in this process', - }); - } + if (!ability.can('view', toCaslResource('Process', process.value))) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to view image in this process', + }); + } - const isInvalidRequest = invalidRequest(request); - if (isInvalidRequest) return isInvalidRequest; + const isInvalidRequest = invalidRequest(request); + if (isInvalidRequest) return isInvalidRequest; - const readImageResult = await readImage(request); - if (readImageResult.error) return readImageResult.error; + const readImageResult = await readImage(request); + if (readImageResult.error) return readImageResult.error; - const newImageFileName = `_image${v4()}.${readImageResult.fileType.ext}`; + const newImageFileName = `_image${v4()}.${readImageResult.fileType.ext}`; - await saveProcessImage(processId, newImageFileName, readImageResult.buffer); + await saveProcessImage(processId, newImageFileName, readImageResult.buffer); - return new NextResponse(newImageFileName, { status: 201, statusText: 'Created' }); + return new NextResponse(newImageFileName, { status: 201, statusText: 'Created' }); + } catch (error) { + return new NextResponse(null, { + status: 500, + statusText: getErrorMessage(error), + }); + } } export async function DELETE( @@ -119,25 +143,36 @@ export async function DELETE( params: { environmentId, processId, imageFileName }, }: { params: { environmentId: string; processId: string; imageFileName: string } }, ) { - const { ability } = await getCurrentEnvironment(environmentId); + try { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; - const process = await getProcess(processId, false); + const { ability } = currentEnvironment.value; - if (!process) { - return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', - }); - } + const process = await getProcess(processId, false); + if (process.isErr()) throw process.error; + + if (!process.value) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } - if (!ability.can('delete', toCaslResource('Process', process))) { + if (!ability.can('delete', toCaslResource('Process', process.value))) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to delete image in this process', + }); + } + + await deleteProcessImage(processId, imageFileName); + + return new NextResponse(null, { status: 200, statusText: 'OK' }); + } catch (error) { return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to delete image in this process', + status: 500, + statusText: getErrorMessage(error), }); } - - await deleteProcessImage(processId, imageFileName); - - return new NextResponse(null, { status: 200, statusText: 'OK' }); } diff --git a/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts b/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts index d826056d9..3cef9237c 100644 --- a/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts +++ b/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts @@ -4,6 +4,8 @@ import { getProcess, getProcessImageFileNames, saveProcessImage } from '@/lib/da import { NextRequest, NextResponse } from 'next/server'; import { v4 } from 'uuid'; import { invalidRequest, readImage } from '../../../image-helpers'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { getErrorMessage } from '@/lib/server-error-handling/user-error'; export async function GET( request: NextRequest, @@ -11,27 +13,36 @@ export async function GET( params: { environmentId, processId }, }: { params: { environmentId: string; processId: string } }, ) { - const { ability } = await getCurrentEnvironment(environmentId); + try { + const currentSpace = await getCurrentEnvironment(environmentId); + if (currentSpace.isErr()) throw currentSpace.error; - const process = await getProcess(processId, false); + const process = await getProcess(processId, false); + if (process.isErr()) throw process.error; - if (!process) { - return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', - }); - } + if (!process.value) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } + + if (!currentSpace.value.ability.can('view', toCaslResource('Process', process.value))) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to view image filenames in this process', + }); + } - if (!ability.can('view', toCaslResource('Process', process))) { + const fileNames = await getProcessImageFileNames(processId); + + return NextResponse.json(fileNames); + } catch (error) { return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to view image filenames in this process', + status: 500, + statusText: getErrorMessage(error), }); } - - const fileNames = await getProcessImageFileNames(processId); - - return NextResponse.json(fileNames); } export async function POST( @@ -40,33 +51,43 @@ export async function POST( params: { environmentId, processId }, }: { params: { environmentId: string; processId: string } }, ) { - const isInvalidRequest = invalidRequest(request); - if (isInvalidRequest) return isInvalidRequest; + try { + const isInvalidRequest = invalidRequest(request); + if (isInvalidRequest) return isInvalidRequest; - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; + const ability = currentEnvironment.value.ability; - const process = await getProcess(processId, false); + const process = await getProcess(processId, false); + if (process.isErr()) throw process.error; - if (!process) { - return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', - }); - } + if (!process.value) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } - if (!ability.can('view', toCaslResource('Process', process))) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to view image in this process', - }); - } + if (!ability.can('view', toCaslResource('Process', process.value))) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to view image in this process', + }); + } - const readImageResult = await readImage(request); - if (readImageResult.error) return readImageResult.error; + const readImageResult = await readImage(request); + if (readImageResult.error) return readImageResult.error; - const imageFileName = `_image${v4()}.${readImageResult.fileType.ext}`; + const imageFileName = `_image${v4()}.${readImageResult.fileType.ext}`; - await saveProcessImage(processId, imageFileName, readImageResult.buffer); + await saveProcessImage(processId, imageFileName, readImageResult.buffer); - return new NextResponse(imageFileName, { status: 201, statusText: 'Created' }); + return new NextResponse(imageFileName, { status: 201, statusText: 'Created' }); + } catch (error) { + return new NextResponse(null, { + status: 500, + statusText: getErrorMessage(error), + }); + } } diff --git a/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts b/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts index 3bc7cf608..53df21f17 100644 --- a/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts +++ b/src/management-system-v2/app/api/private/delete-inactive-guests/route.ts @@ -18,11 +18,12 @@ export async function POST(request: NextRequest) { return NextResponse.json({ error: 'Unauthorized: Invalid Bearer token' }, { status: 403 }); } - const { count } = await deleteInactiveGuestUsers(GUESET_INACTIVE_TIME); + const result = await deleteInactiveGuestUsers(GUESET_INACTIVE_TIME); + if (result.isErr()) throw result.error; return NextResponse.json( { - message: `${count} guest users deleted`, + message: `${result.value.count} guest users deleted`, }, { status: 200 }, ); diff --git a/src/management-system-v2/app/api/private/file-manager/route.ts b/src/management-system-v2/app/api/private/file-manager/route.ts index 77bf5c752..5d48f4ba7 100644 --- a/src/management-system-v2/app/api/private/file-manager/route.ts +++ b/src/management-system-v2/app/api/private/file-manager/route.ts @@ -3,7 +3,6 @@ import { getCurrentEnvironment } from '@/components/auth'; import { toCaslResource } from '@/lib/ability/caslAbility'; import { NextRequest, NextResponse } from 'next/server'; import { Readable } from 'node:stream'; -import type { ReadableStream } from 'node:stream/web'; import jwt from 'jsonwebtoken'; import { TokenPayload } from '@/lib/sharing/process-sharing'; import { getProcess } from '@/lib/data/processes'; @@ -13,244 +12,271 @@ import { retrieveEntityFile, saveEntityFileOrGetPresignedUrl, } from '@/lib/data/file-manager-facade'; +import { getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; const MAX_FILE_SIZE = 10 * 1024 * 1024; // 10 MB const MAX_IMAGE_SIZE = 2 * 1024 * 1024; // 2 MB export async function GET(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const entityId = searchParams.get('entityId'); - const entityType = searchParams.get('entityType'); - const environmentId = searchParams.get('environmentId') || 'unauthenticated'; - const filePath = searchParams.get('filePath') || undefined; - if ( - !entityId || - !entityType || - !environmentId || - (entityType === EntityType.PROCESS && !filePath) - ) { - return new NextResponse(null, { - status: 400, - statusText: 'entityId, entityType, environmentId and filePath required as URL search params', - }); - } - - if (entityType === EntityType.PROCESS) { - let canAccess = false; - - const processMeta = await getProcess(entityId, environmentId, true); // true --> skip the validity check as it will be done below - if (!processMeta || 'error' in processMeta) { + try { + const searchParams = request.nextUrl.searchParams; + const entityId = searchParams.get('entityId'); + const entityType = searchParams.get('entityType'); + const environmentId = searchParams.get('environmentId') || 'unauthenticated'; + const filePath = searchParams.get('filePath') || undefined; + if ( + !entityId || + !entityType || + !environmentId || + (entityType === EntityType.PROCESS && !filePath) + ) { return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', + status: 400, + statusText: + 'entityId, entityType, environmentId and filePath required as URL search params', }); } - if (environmentId !== 'unauthenticated') { - const { ability } = await getCurrentEnvironment(environmentId); + if (entityType === EntityType.PROCESS) { + let canAccess = false; - canAccess = ability.can('view', toCaslResource('Process', processMeta)); - } + const processMeta = await getProcess(entityId, environmentId, true); // true --> skip the validity check as it will be done below + if (!processMeta || 'error' in processMeta) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } - // if the user is not an owner check if they have access if a share token is provided in the query data of the url - const shareToken = searchParams.get('shareToken'); - if (!canAccess && shareToken) { - const key = process.env.SHARING_ENCRYPTION_SECRET!; - const { - processId: shareProcessId, - embeddedMode, - timestamp, - } = jwt.verify(shareToken, key!) as TokenPayload; - canAccess = - !embeddedMode && shareProcessId === entityId && timestamp === processMeta.shareTimestamp; - } + if (environmentId !== 'unauthenticated') { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; + const { ability } = currentEnvironment.value; - if (!canAccess) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to access files in this process', - }); - } - } + canAccess = ability.can('view', toCaslResource('Process', processMeta)); + } - try { - const data = await retrieveEntityFile(entityType as EntityType, entityId, filePath); + // if the user is not an owner check if they have access if a share token is provided in the query data of the url + const shareToken = searchParams.get('shareToken'); + if (!canAccess && shareToken) { + const key = process.env.SHARING_ENCRYPTION_SECRET!; + const { + processId: shareProcessId, + embeddedMode, + timestamp, + } = jwt.verify(shareToken, key!) as TokenPayload; + canAccess = + !embeddedMode && shareProcessId === entityId && timestamp === processMeta.shareTimestamp; + } - const fileType = await fileTypeFromBuffer(data as Buffer); - if (!fileType) { - return new NextResponse(null, { - status: 415, - statusText: 'Cannot read file type of requested file', - }); + if (!canAccess) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to access files in this process', + }); + } } - let mimeType: string = fileType.mime; - - if (fileType.mime === 'application/xml' && filePath?.endsWith('.svg')) - mimeType = 'image/svg+xml'; - const headers = new Headers(); - headers.set('Content-Type', mimeType); - return new NextResponse(data, { status: 200, statusText: 'OK', headers }); - } catch (error: any) { - console.error('Error retrieving file:', error); + try { + const data = await retrieveEntityFile(entityType as EntityType, entityId, filePath); - if (error.message.includes('File') && error.message.includes('does not exist')) { + const fileType = await fileTypeFromBuffer(data as Buffer); + if (!fileType) { + return new NextResponse(null, { + status: 415, + statusText: 'Cannot read file type of requested file', + }); + } + let mimeType: string = fileType.mime; + + if (fileType.mime === 'application/xml' && filePath?.endsWith('.svg')) + mimeType = 'image/svg+xml'; + + const headers = new Headers(); + headers.set('Content-Type', mimeType); + return new NextResponse(data, { status: 200, statusText: 'OK', headers }); + } catch (error: any) { + console.error('Error retrieving file:', error); + + if (error.message.includes('File') && error.message.includes('does not exist')) { + return new NextResponse(null, { + status: 404, + statusText: 'File not found', + }); + } return new NextResponse(null, { - status: 404, - statusText: 'File not found', + status: 500, + statusText: 'Internal Server Error', }); } + } catch (error) { return new NextResponse(null, { status: 500, - statusText: 'Internal Server Error', + statusText: getErrorMessage(error), }); } } export async function PUT(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const entityId = searchParams.get('entityId'); - const entityType = searchParams.get('entityType'); - const environmentId = searchParams.get('environmentId'); - const filePath = searchParams.get('filePath'); - const saveWithoutSavingReference = !!searchParams.get('saveWithoutSavingReference'); - - if (!entityId || !environmentId || !entityType || !filePath) { - return new NextResponse(null, { - status: 400, - statusText: 'entityId, entityType, environmentId and filePath required as URL search params', - }); - } - - const body = request.body; - if (!body) - return new NextResponse(null, { - status: 400, - statusText: 'No file data provided in request body', - }); - - const { ability } = await getCurrentEnvironment(environmentId); - if (entityType === EntityType.PROCESS) { - const process = await getProcess(entityId, environmentId); - if (!process) { + try { + const searchParams = request.nextUrl.searchParams; + const entityId = searchParams.get('entityId'); + const entityType = searchParams.get('entityType'); + const environmentId = searchParams.get('environmentId'); + const filePath = searchParams.get('filePath'); + const saveWithoutSavingReference = !!searchParams.get('saveWithoutSavingReference'); + + if (!entityId || !environmentId || !entityType || !filePath) { return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', + status: 400, + statusText: + 'entityId, entityType, environmentId and filePath required as URL search params', }); } - if (!ability.can('view', toCaslResource('Process', process))) { + + const body = request.body; + if (!body) return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to view image in this process', + status: 400, + statusText: 'No file data provided in request body', }); - } - } - - // NOTE: This may need changing - // @ts-expect-error ReadableStream in next.js' request isn't quite compatible with Readable.fromWeb (node:stream) - const reader = Readable.fromWeb(body); - const chunks: Uint8Array[] = []; - let totalLength = 0; + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; + const { ability } = currentEnvironment.value; + + if (entityType === EntityType.PROCESS) { + const process = await getProcess(entityId, environmentId); + if (!process) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } + if (!ability.can('view', toCaslResource('Process', process))) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to view image in this process', + }); + } + } - for await (const chunk of reader) { - if (chunk) { - chunks.push(chunk); - totalLength += chunk.length; - if (totalLength > MAX_FILE_SIZE) { - // For some reason, after calling destroy the http response is not sent, causing the request to hang - reader.pause(); + // NOTE: This may need changing + // @ts-expect-error ReadableStream in next.js' request isn't quite compatible with Readable.fromWeb (node:stream) + const reader = Readable.fromWeb(body); + + const chunks: Uint8Array[] = []; + let totalLength = 0; + + for await (const chunk of reader) { + if (chunk) { + chunks.push(chunk); + totalLength += chunk.length; + if (totalLength > MAX_FILE_SIZE) { + // For some reason, after calling destroy the http response is not sent, causing the request to hang + reader.pause(); + } } } - } - // The return doesn't work inside of the iterator for loop - if (totalLength > MAX_FILE_SIZE) - return new NextResponse(null, { - status: 413, - statusText: `Allowed file size of ${MAX_FILE_SIZE / (1024 * 1024)} MB exceeded`, - }); + // The return doesn't work inside of the iterator for loop + if (totalLength > MAX_FILE_SIZE) + return new NextResponse(null, { + status: 413, + statusText: `Allowed file size of ${MAX_FILE_SIZE / (1024 * 1024)} MB exceeded`, + }); - // Proceed with processing if the size limit is not exceeded - const buffer = Buffer.concat( - chunks.map((chunk) => Buffer.from(chunk)), - totalLength, - ); + // Proceed with processing if the size limit is not exceeded + const buffer = Buffer.concat( + chunks.map((chunk) => Buffer.from(chunk)), + totalLength, + ); - const fileType = await fileTypeFromBuffer(buffer); - if (!fileType) { - return new NextResponse(null, { - status: 415, - statusText: 'Cannot store file with unknown file type', - }); - } + const fileType = await fileTypeFromBuffer(buffer); + if (!fileType) { + return new NextResponse(null, { + status: 415, + statusText: 'Cannot store file with unknown file type', + }); + } - try { - const res = await saveEntityFileOrGetPresignedUrl( - entityType as EntityType, - entityId, - fileType.mime, - filePath, - buffer, - { saveWithoutSavingReference }, - ); + try { + const res = await saveEntityFileOrGetPresignedUrl( + entityType as EntityType, + entityId, + fileType.mime, + filePath, + buffer, + { saveWithoutSavingReference }, + ); - if ('error' in res) throw new Error((res.error as any).message); + if ('error' in res) throw new Error((res.error as any).message); - if (!res.filePath) { - throw new Error('No file name returned'); - } + if (!res.filePath) { + throw new Error('No file name returned'); + } - const { filePath: newFileName } = res; + const { filePath: newFileName } = res; - return new NextResponse(newFileName, { - status: 200, - statusText: 'OK', - }); + return new NextResponse(newFileName, { + status: 200, + statusText: 'OK', + }); + } catch (error) { + console.error('Error saving file:', error); + return new NextResponse(null, { status: 500, statusText: 'Internal Server Error' }); + } } catch (error) { - console.error('Error saving file:', error); - return new NextResponse(null, { status: 500, statusText: 'Internal Server Error' }); + return new NextResponse(null, { status: 500, statusText: getErrorMessage(error) }); } } export async function DELETE(request: NextRequest) { - const searchParams = request.nextUrl.searchParams; - const entityId = searchParams.get('entityId'); - const entityType = searchParams.get('entityType'); - const environmentId = searchParams.get('environmentId'); - const filePath = searchParams.get('filePath '); - - if (!entityId || !entityType || !environmentId || !filePath) { - return new NextResponse(null, { - status: 400, - statusText: 'entityId, entityType, environmentId and filePath required as URL search params', - }); - } - - const { ability } = await getCurrentEnvironment(environmentId); - if (entityType === EntityType.PROCESS) { - const process = await getProcess(entityId, environmentId); + try { + const searchParams = request.nextUrl.searchParams; + const entityId = searchParams.get('entityId'); + const entityType = searchParams.get('entityType'); + const environmentId = searchParams.get('environmentId'); + const filePath = searchParams.get('filePath '); - if (!process) { + if (!entityId || !entityType || !environmentId || !filePath) { return new NextResponse(null, { - status: 404, - statusText: 'Process with this id does not exist.', + status: 400, + statusText: + 'entityId, entityType, environmentId and filePath required as URL search params', }); } - if (!ability.can('delete', toCaslResource('Process', process))) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to delete image in this process', - }); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) throw currentEnvironment.error; + const { ability } = currentEnvironment.value; + + if (entityType === EntityType.PROCESS) { + const process = await getProcess(entityId, environmentId); + + if (!process) { + return new NextResponse(null, { + status: 404, + statusText: 'Process with this id does not exist.', + }); + } + + if (!ability.can('delete', toCaslResource('Process', process))) { + return new NextResponse(null, { + status: 403, + statusText: 'Not allowed to delete image in this process', + }); + } } - } - try { - await deleteEntityFile(entityType as EntityType, entityId, filePath); - return new NextResponse(null, { status: 200, statusText: 'OK' }); + try { + await deleteEntityFile(entityType as EntityType, entityId, filePath); + return new NextResponse(null, { status: 200, statusText: 'OK' }); + } catch (error) { + console.error('Error deleting file:', error); + return new NextResponse(null, { status: 500, statusText: 'Internal Server Error' }); + } } catch (error) { - console.error('Error deleting file:', error); - return new NextResponse(null, { status: 500, statusText: 'Internal Server Error' }); + return new NextResponse(null, { status: 500, statusText: getErrorMessage(error) }); } } diff --git a/src/management-system-v2/app/api/register-new-user/route.ts b/src/management-system-v2/app/api/register-new-user/route.ts index e35fd41ff..5a6a7e528 100644 --- a/src/management-system-v2/app/api/register-new-user/route.ts +++ b/src/management-system-v2/app/api/register-new-user/route.ts @@ -7,6 +7,7 @@ import db from '@/lib/data/db'; import { addUser, setUserPassword } from '@/lib/data/db/iam/users'; import { getTokenHash, notExpired } from '@/lib/email-verification-tokens/utils'; import { env } from '@/lib/ms-config/env-vars'; +import { getErrorMessage } from '@/lib/server-error-handling/user-error'; // TODO: maybe add PRETTIER error handling @@ -22,7 +23,12 @@ export const GET = async (req: Request) => { const tokenHash = await getTokenHash(token); - const verificationToken = await getEmailVerificationToken({ token: tokenHash, identifier }); + const _verificationToken = await getEmailVerificationToken({ token: tokenHash, identifier }); + if (_verificationToken.isErr()) throw _verificationToken.error; + if (!_verificationToken.value) + return Response.json({ message: 'Bad request' }, { status: 400 }); + const verificationToken = _verificationToken.value!; + if (verificationToken?.type !== 'register_new_user') return Response.json({ message: 'Bad request' }, { status: 400 }); @@ -42,9 +48,16 @@ export const GET = async (req: Request) => { }, tx, ); + if (user.isErr()) throw user.error; - if (verificationToken.passwordHash) - await setUserPassword(user.id, verificationToken.passwordHash, tx); + if (verificationToken.passwordHash) { + const setPassword = await setUserPassword( + user.value.id, + verificationToken.passwordHash, + tx, + ); + if (setPassword.isErr()) throw setPassword.error; + } // We can't delete the token yet, because we need it to exist in the db for the redirect to // nextAuth's email flow. We set the expiration to 5 minutes in the future, so that it can't @@ -53,11 +66,12 @@ export const GET = async (req: Request) => { // username & email, and if the user manages to use it again, nothing bad will happen, the // user will just have 2 or more users in the db. - await updateEmailVerificationTokenExpiration( + const updateToken = await updateEmailVerificationTokenExpiration( { token: tokenHash, identifier }, new Date(Date.now() + 5 * 60 * 1000), tx, ); + if (updateToken.isErr()) throw updateToken.error; }); // The user is already created, now we redirect to nextAuth, so that it can set the cookies @@ -76,7 +90,7 @@ export const GET = async (req: Request) => { redirectUrl = nextAuthEmailRedirect.toString(); } catch (e) { console.error(e); - return Response.json({ message: 'Error registeering user' }, { status: 500 }); + return Response.json({ message: getErrorMessage(e) }, { status: 500 }); } redirect(redirectUrl); diff --git a/src/management-system-v2/app/change-email/page.tsx b/src/management-system-v2/app/change-email/page.tsx index 52322dd3d..49430a8e1 100644 --- a/src/management-system-v2/app/change-email/page.tsx +++ b/src/management-system-v2/app/change-email/page.tsx @@ -7,6 +7,7 @@ import ChangeEmailCard from './change-email-card'; import { Card } from 'antd'; import Link from 'next/link'; import Content from '@/components/content'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const searchParamsSchema = z.object({ email: z.string().email(), token: z.string() }); @@ -15,7 +16,11 @@ export default async function ChangeEmailPage({ searchParams }: { searchParams: if (!parsedSearchParams.success) redirect('/'); const { email, token } = parsedSearchParams.data; - const { session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { session } = currentUser.value; const userId = session?.user.id; if (!userId) return ( @@ -41,12 +46,15 @@ export default async function ChangeEmailPage({ searchParams }: { searchParams: identifier: email, token: await getTokenHash(token), }); + if (verificationToken.isErr()) { + return errorResponse(verificationToken); + } if ( - !verificationToken || - verificationToken.type !== 'change_email' || - verificationToken.userId !== userId || - !(await notExpired(verificationToken)) + !verificationToken.value || + verificationToken.value.type !== 'change_email' || + verificationToken.value.userId !== userId || + !(await notExpired(verificationToken.value)) ) redirect('/'); diff --git a/src/management-system-v2/app/create-organization/page.tsx b/src/management-system-v2/app/create-organization/page.tsx index 5d4591523..eb530598c 100644 --- a/src/management-system-v2/app/create-organization/page.tsx +++ b/src/management-system-v2/app/create-organization/page.tsx @@ -7,14 +7,24 @@ import { getErrorMessage, userError } from '@/lib/server-error-handling/user-err import { getMSConfig } from '@/lib/ms-config/ms-config'; import { notFound } from 'next/navigation'; import { env } from '@/lib/ms-config/env-vars'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; async function createInactiveEnvironment(data: UserOrganizationEnvironmentInput) { 'use server'; try { const user = await getCurrentUser(); - if (user.session?.user && !user.session?.user.isGuest) + if (user.isErr()) { + return userError(getErrorMessage(user.error)); + } + if (user.value.session?.user && !user.value.session?.user.isGuest) return userError('This function is only for guest users and users that are not signed in'); - return addEnvironment({ ...data, isOrganization: true, isActive: false }); + + const result = await addEnvironment({ ...data, isOrganization: true, isActive: false }); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { const message = getErrorMessage(e); return userError(message); @@ -33,7 +43,11 @@ const Page = async () => { return notFound(); } - const { session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { session } = currentUser.value; const needsToAuthenticate = !session?.user || session?.user.isGuest; let providers = getProviders(); diff --git a/src/management-system-v2/app/shared-viewer/documentation-page.tsx b/src/management-system-v2/app/shared-viewer/documentation-page.tsx index 86aa375ad..ae65613ad 100644 --- a/src/management-system-v2/app/shared-viewer/documentation-page.tsx +++ b/src/management-system-v2/app/shared-viewer/documentation-page.tsx @@ -14,14 +14,9 @@ import { Button, Tooltip, Typography, Space, Grid } from 'antd'; import { PrinterOutlined } from '@ant-design/icons'; import Content from '@/components/content'; - -import { getProcess } from '@/lib/data/db/process'; import { useRouter } from 'next/navigation'; - import { getSVGFromBPMN } from '@/lib/process-export/util'; - import styles from './documentation-page.module.scss'; - import { getRootFromElement, getDefinitionsVersionInformation } from '@proceed/bpmn-helper'; import SettingsModal, { settingsOptions, SettingsOption } from './settings-modal'; @@ -38,6 +33,7 @@ import { getElementSVG, } from './documentation-page-utils'; import { Environment } from '@/lib/data/environment-schema'; +import { Process } from '@/lib/data/process-schema'; /** * Import the Editor asynchronously since it implicitly uses browser logic which leads to errors when this file is loaded on the server @@ -53,7 +49,7 @@ const markdownEditor: Promise = : (Promise.resolve(null) as any); type BPMNSharedViewerProps = { - processData: Awaited>; + processData: Process; isOwner: boolean; userWorkspaces: Environment[]; defaultSettings?: SettingsOption; diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index da344e905..d01fd1424 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -9,6 +9,7 @@ import { ImportsInfo } from './documentation-page-utils'; import BPMNCanvas from '@/components/bpmn-canvas'; import { Process } from '@/lib/data/process-schema'; import ErrorMessage from '../../components/error-message'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; import styles from './page.module.scss'; import Layout from '@/app/(dashboard)/[environmentId]/layout-client'; @@ -21,6 +22,8 @@ import { getDefinitionsAndProcessIdForEveryCallActivity } from '@proceed/bpmn-he import { SettingsOption } from './settings-modal'; import { asyncMap } from '@/lib/helpers/javascriptHelpers'; import { env } from '@/lib/ms-config/env-vars'; +import { getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; +import { Result } from 'neverthrow'; interface PageProps { searchParams: { @@ -45,20 +48,31 @@ const getProcessInfo = async ( isImport: boolean, versionId?: string, ) => { - const { session, userId } = await getCurrentUser(); - + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { session, userId } = currentUser.value; let spaceId; let isOwner = false; let processData; - // check if there is a session (=> the user is already logged in) if (session) { - const { ability, activeEnvironment } = await getCurrentEnvironment(session?.user.id); + const currentSpace = await getCurrentEnvironment(session?.user.id); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; + ({ spaceId } = activeEnvironment); // get all the processes the user has access to const ownedProcesses = await getProcesses(spaceId, ability); + if (ownedProcesses.isErr()) { + return userError(getErrorMessage(ownedProcesses.error)); + } + // check if the current user is the owner of the process(/has access to the process) => if yes give access regardless of sharing status - isOwner = ownedProcesses.some((process) => process.id === definitionId); + isOwner = ownedProcesses.value.some((process) => process.id === definitionId); } if (isOwner) { @@ -70,7 +84,11 @@ const getProcessInfo = async ( ? await getProcessVersionBpmn(definitionId, versionId) : await getProcessBpmn(definitionId); - processData = { ...processMetaData, bpmn }; + if (bpmn.isErr()) { + return userError(getErrorMessage(bpmn.error)); + } + + processData = { ...processMetaData, bpmn: bpmn.value }; } } else { // the user has no regular access to the process so get the process data from the sharing api @@ -83,8 +101,8 @@ const getProcessInfo = async ( if ( // bypass the timestamp check for imports !isImport && - ((embeddedMode && timestamp !== processData.allowIframeTimestamp) || - (!embeddedMode && timestamp !== processData.shareTimestamp)) + ((embeddedMode && timestamp !== processData.value.allowIframeTimestamp) || + (!embeddedMode && timestamp !== processData.value.shareTimestamp)) ) { return ; } @@ -126,20 +144,32 @@ const getImportInfos = async (bpmn: string, knownInfos: ImportsInfo) => { const SharedViewer = async ({ searchParams }: PageProps) => { const { token, version, settings } = searchParams; - const { session, userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { session, userId } = currentUser.value; if (typeof token !== 'string') { return ; } - const userEnvironments: Environment[] = [(await getEnvironmentById(userId))!]; + const personalEnvironment = await getEnvironmentById(userId); + if (personalEnvironment.isErr()) return errorResponse(personalEnvironment); + + const userEnvironments: Environment[] = [personalEnvironment.value]; + const userOrgEnvs = await getUserOrganizationEnvironments(userId); + if (userOrgEnvs.isErr()) return errorResponse(userOrgEnvs); - const orgEnvironments = await asyncMap( - userOrgEnvs, - async (environmentId) => (await getEnvironmentById(environmentId))!, + const orgEnvironments = Result.combine( + await asyncMap( + userOrgEnvs.value, + async (environmentId) => (await getEnvironmentById(environmentId))!, + ), ); + if (orgEnvironments.isErr()) return errorResponse(orgEnvironments); - userEnvironments.push(...orgEnvironments); + userEnvironments.push(...orgEnvironments.value); let isOwner = false; diff --git a/src/management-system-v2/app/shared-viewer/process-document.tsx b/src/management-system-v2/app/shared-viewer/process-document.tsx index 09b1f055c..874c57e8e 100644 --- a/src/management-system-v2/app/shared-viewer/process-document.tsx +++ b/src/management-system-v2/app/shared-viewer/process-document.tsx @@ -18,6 +18,7 @@ import { useEnvironment } from '@/components/auth-can'; import { EntityType } from '@/lib/helpers/fileManagerHelpers'; import { useFileManager } from '@/lib/useFileManager'; import { fromCustomUTCString } from '@/lib/helpers/timeHelper'; +import { Process } from '@/lib/data/process-schema'; export type VersionInfo = { id?: string; @@ -27,7 +28,7 @@ export type VersionInfo = { }; type ProcessDocumentProps = { - processData: Awaited>; + processData: Process; settings: ActiveSettings; processHierarchy?: ElementInfo; version: VersionInfo; diff --git a/src/management-system-v2/app/shared-viewer/workspace-selection.tsx b/src/management-system-v2/app/shared-viewer/workspace-selection.tsx index 7310452d4..e977cf18a 100644 --- a/src/management-system-v2/app/shared-viewer/workspace-selection.tsx +++ b/src/management-system-v2/app/shared-viewer/workspace-selection.tsx @@ -18,9 +18,11 @@ import { FolderTreeNode, getSpaceFolderTree } from '@/lib/data/folders'; import { useFileManager } from '@/lib/useFileManager'; import { EntityType } from '@/lib/helpers/fileManagerHelpers'; import { useSession } from '@/components/auth-can'; +import { Process } from '@/lib/data/process-schema'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; type WorkspaceSelectionProps = { - processData: Awaited>; + processData: Process; versionInfo: VersionInfo; workspaces: Environment[]; }; @@ -65,7 +67,10 @@ const WorkspaceSelection: React.FC< const handleWorkspaceClick = async (workspace: Environment) => { setSelectedWorkspace(workspace); onWorkspaceSelect(workspace); + const spaceFolderTree = await getSpaceFolderTree(workspace.id); + if (isUserErrorResponse(spaceFolderTree)) return; + setSelectedSpaceFolderTree(spaceFolderTree); onFolderSelect(null); }; diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index 88ddd048e..44af18ed1 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -6,6 +6,7 @@ import { Card, Result } from 'antd'; import { redirect } from 'next/navigation'; import ProcessTransferButtons from './transfer-processes-confirmation-buttons'; import { getGuestReference } from '@/lib/reference-guest-user-token'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; export default async function TransferProcessesPage({ searchParams, @@ -15,7 +16,11 @@ export default async function TransferProcessesPage({ referenceToken?: string; }; }) { - const { userId, session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + const { userId, session } = currentUser.value; if (!session) redirect('api/auth/signin'); if (session.user.isGuest) redirect('/'); @@ -44,17 +49,23 @@ export default async function TransferProcessesPage({ if (!guestId || guestId === userId) redirect(callbackUrl); const possibleGuest = await getUserById(guestId); + if (possibleGuest.isErr()) { + return errorResponse(possibleGuest); + } // possibleGuest might be a normal user, this would happen if the user signed in with an existing // accocunt, generating the token above, and before using it, he signed in with a new account. // We only go further then this redirect, if the user signed in with an account that was // already linked to an existing user - if (!possibleGuest || !possibleGuest.isGuest) redirect(callbackUrl); + if (!possibleGuest || !possibleGuest.value.isGuest) redirect(callbackUrl); // NOTE: this ignores folders const guestProcesses = await getProcesses(guestId); + if (guestProcesses.isErr()) { + return errorResponse(guestProcesses); + } // If the guest has no processes -> nothing to do - if (guestProcesses.length === 0) redirect(callbackUrl); + if (guestProcesses.value.length === 0) redirect(callbackUrl); return ( @@ -62,7 +73,8 @@ export default async function TransferProcessesPage({ title="Would you like to transfer your processes?" style={{ maxWidth: '70ch', margin: 'auto' }} > - Your guest account had {guestProcesses.length} process{guestProcesses.length !== 1 && 'es'}. + Your guest account had {guestProcesses.value.length} process + {guestProcesses.value.length !== 1 && 'es'}.
Would you like to transfer them to your account? diff --git a/src/management-system-v2/app/transfer-processes/server-actions.ts b/src/management-system-v2/app/transfer-processes/server-actions.ts index c44ecad35..f3a06a1ac 100644 --- a/src/management-system-v2/app/transfer-processes/server-actions.ts +++ b/src/management-system-v2/app/transfer-processes/server-actions.ts @@ -1,5 +1,4 @@ 'use server'; - import { getCurrentUser } from '@/components/auth'; import { Folder } from '@/lib/data/folder-schema'; import { getFolders, getRootFolder, moveFolder, updateFolderMetaData } from '@/lib/data/db/folders'; @@ -7,11 +6,16 @@ import { getProcesses, updateProcess } from '@/lib/data/db/process'; import { getUserById, deleteUser } from '@/lib/data/db/iam/users'; import { Process } from '@/lib/data/process-schema'; import { getGuestReference } from '@/lib/reference-guest-user-token'; -import { UserErrorType, userError } from '@/lib/server-error-handling/user-error'; +import { UserErrorType, getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; import { redirect } from 'next/navigation'; +import db from '@/lib/data/db'; export async function transferProcesses(referenceToken: string, callbackUrl: string = '/') { - const { session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { session } = currentUser.value; if (!session) return userError("You're not signed in", UserErrorType.PermissionError); if (session.user.isGuest) return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); @@ -23,44 +27,91 @@ export async function transferProcesses(referenceToken: string, callbackUrl: str if (guestId === session.user.id) redirect(callbackUrl); const possibleGuest = await getUserById(guestId); - if (!possibleGuest || !possibleGuest.isGuest) + if (possibleGuest.isErr()) { + return userError(getErrorMessage(possibleGuest.error)); + } + if (!possibleGuest.value || !possibleGuest.value.isGuest) return userError('Invalid guest id', UserErrorType.PermissionError); // Processes and folders under root folder of guest space guet their folderId changed to the // root folder of the new owner space, for the rest we just update the environmentId - const userRootFolderId = (await getRootFolder(session.user.id)).id; - const guestRootFolderId = (await getRootFolder(guestId)).id; + const userRootFolder = await getRootFolder(session.user.id); + if (userRootFolder.isErr()) { + return userError(getErrorMessage(userRootFolder.error)); + } + + const guestRootFolder = await getRootFolder(guestId); + if (guestRootFolder.isErr()) { + return userError(getErrorMessage(guestRootFolder.error)); + } // no ability check necessary, owners of personal spaces can do anything const guestProcesses = await getProcesses(guestId); - for (const process of guestProcesses) { - const processUpdate: Partial = { - environmentId: session.user.id, - creatorId: session.user.id, - }; - if (process.folderId === guestRootFolderId) processUpdate.folderId = userRootFolderId; - await updateProcess(process.id, processUpdate); + if (guestProcesses.isErr()) { + return userError(getErrorMessage(guestProcesses.error)); } - const guestFolders = await getFolders(guestId); - for (const folder of guestFolders) { - if (folder.id === guestRootFolderId) continue; - - const folderData: Partial = { createdBy: session.user.id }; - - if (folder.parentId === guestRootFolderId) moveFolder(folder.id, userRootFolderId); - else folderData.environmentId = session.user.id; - - updateFolderMetaData(folder.id, folderData); + try { + await db.$transaction(async (tx) => { + for (const process of guestProcesses.value) { + const processUpdate: Partial = { + environmentId: session.user.id, + creatorId: session.user.id, + }; + + if (process.folderId === guestRootFolder.value.id) { + processUpdate.folderId = userRootFolder.value.id; + } + const result = await updateProcess(process.id, processUpdate, tx); + if (result.isErr()) { + throw result.error; + } + } + + const guestFolders = await getFolders(guestId); + if (guestFolders.isErr()) { + throw guestFolders.error; + } + + for (const folder of guestFolders.value) { + // skip the guest's root folder + if (folder.id === guestRootFolder.value.id) continue; + + const folderData: Partial = { createdBy: session.user.id }; + + if (folder.parentId === guestRootFolder.value.id) { + const moveResult = await moveFolder(folder.id, userRootFolder.value.id, undefined, tx); + if (moveResult?.isErr()) { + throw moveResult.error; + } + } else { + folderData.environmentId = session.user.id; + } + + const updateResult = await updateFolderMetaData(folder.id, folderData, undefined, tx); + if (updateResult.isErr()) { + throw updateResult.error; + } + } + + const deleteResult = await deleteUser(guestId, tx); + if (deleteResult.isErr()) { + throw deleteResult.error; + } + }); + } catch (error) { + return userError(getErrorMessage(error)); } - deleteUser(guestId); - redirect(callbackUrl); } export async function discardProcesses(referenceToken: string, redirectUrl: string = '/') { - const { session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { session } = currentUser.value; if (!session) return userError("You're not signed in", UserErrorType.PermissionError); if (session.user.isGuest) return userError("You can't be a guest to transfer processes", UserErrorType.PermissionError); @@ -72,10 +123,17 @@ export async function discardProcesses(referenceToken: string, redirectUrl: stri if (guestId === session.user.id) redirect(redirectUrl); const possibleGuest = await getUserById(guestId); - if (!possibleGuest || !possibleGuest.isGuest) + if (possibleGuest.isErr()) { + return userError(getErrorMessage(possibleGuest.error)); + } + + if (!possibleGuest.value || !possibleGuest.value.isGuest) return userError('Invalid guest id', UserErrorType.PermissionError); - deleteUser(guestId); + const deleteResult = await deleteUser(guestId); + if (deleteResult.isErr()) { + return userError(getErrorMessage(deleteResult.error)); + } redirect(redirectUrl); } diff --git a/src/management-system-v2/components/auth.tsx b/src/management-system-v2/components/auth.tsx index f3d79eb68..76f71c72f 100644 --- a/src/management-system-v2/components/auth.tsx +++ b/src/management-system-v2/components/auth.tsx @@ -15,9 +15,10 @@ import * as noIamUser from '@/lib/no-iam-user'; import { getUserById } from '@/lib/data/db/iam/users'; import { cookies } from 'next/headers'; import { getMSConfig } from '@/lib/ms-config/ms-config'; -import { UIError as UserUIError } from '@/lib/ui-error'; import { packedStaticRules } from '@/lib/authorization/caslRules'; -import { ok } from 'neverthrow'; +import { err, ok } from 'neverthrow'; +import { UserFacingError } from '@/lib/server-error-handling/user-error'; +import { Prettify } from '@/lib/typescript-utils'; export const getCurrentUser = cache(async () => { if (!env.PROCEED_PUBLIC_IAM_ACTIVE) { @@ -79,6 +80,7 @@ export const getSystemAdminRules = cache((isOrganization: boolean) => { } }); +type a = Awaited>; // TODO: To enable PPR move the session redirect into this function, so it will // be called when the session is first accessed and everything above can PPR. For // permissions, each server component should check its permissions anyway, for @@ -95,7 +97,7 @@ export const getCurrentEnvironment = cache( ) => { const currentUser = await getCurrentUser(); if (currentUser.isErr()) { - return currentUser; + return err('pepe'); } const { userId, systemAdmin } = currentUser.value; @@ -116,13 +118,13 @@ export const getCurrentEnvironment = cache( if (userId && !isOrganization && !env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { // Note: will be undefined for not logged in users const userOrgs = await getUserOrganizationEnvironments(userId); - if (userOrgs.isErr()) { - return userOrgs; - } + // if (userOrgs.isErr()) { + // return userOrgs; + // } if (userOrgs.value.length === 0) { if (env.PROCEED_PUBLIC_IAM_ONLY_ONE_ORGANIZATIONAL_SPACE) { - throw new UserUIError('You are not part of an organization.'); + return err(new UserFacingError('You are not part of an organization.')); } else { return redirect(`/create-organization`); } @@ -146,7 +148,7 @@ export const getCurrentEnvironment = cache( if (!userId || !isMember(decodeURIComponent(spaceIdParam), userId)) { switch (opts?.permissionErrorHandling.action) { case 'throw-error': - throw new Error('User does not have access to this environment'); + return err(new Error('User does not have access to this environment')); case 'redirect': default: if (opts.permissionErrorHandling.redirectUrl) @@ -158,9 +160,12 @@ export const getCurrentEnvironment = cache( } const ability = await getAbilityForUser(userId, activeSpace); + if (ability.isErr()) { + return ability; + } return ok({ - ability, + ability: ability.value, activeEnvironment: { spaceId: activeSpace, isOrganization }, }); }, diff --git a/src/management-system-v2/components/process-modal.tsx b/src/management-system-v2/components/process-modal.tsx index 5f884639e..69ed80f03 100644 --- a/src/management-system-v2/components/process-modal.tsx +++ b/src/management-system-v2/components/process-modal.tsx @@ -18,7 +18,7 @@ import { Skeleton, } from 'antd'; import { MdArrowBackIos, MdArrowForwardIos } from 'react-icons/md'; -import { UserError } from '@/lib/server-error-handling/user-error'; +import { UserError, isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import { useAddControlCallback } from '@/lib/controls-store'; import { checkIfProcessExistsByName } from '@/lib/data/processes'; import { useEnvironment } from './auth-can'; @@ -110,6 +110,7 @@ const ProcessModal = < spaceId: environment.spaceId, userId: session.data?.user.id!, }); + if (isUserErrorResponse(existsResults)) return; existsResults.forEach((exists, index) => { if (exists) { @@ -417,7 +418,7 @@ const ProcessInputs = ({ index, initialName, readonly = false }: ProcessInputsPr folderId: currentFolderId, }); - if (!exists) { + if (isUserErrorResponse(exists) || !exists) { callback(); return; } diff --git a/src/management-system-v2/components/processes/index.tsx b/src/management-system-v2/components/processes/index.tsx index 1335e25e5..3d5e430c4 100644 --- a/src/management-system-v2/components/processes/index.tsx +++ b/src/management-system-v2/components/processes/index.tsx @@ -22,7 +22,6 @@ import { Card, Badge, Divider, - MenuProps, Typography, } from 'antd'; import { InfoCircleOutlined } from '@ant-design/icons'; @@ -34,7 +33,6 @@ import { AppstoreOutlined, FolderOutlined, FileOutlined, - FolderFilled, ShareAltOutlined, } from '@ant-design/icons'; import IconView from '@/components/process-icon-list'; @@ -57,7 +55,7 @@ import { import ProcessModal from '@/components/process-modal'; import ConfirmationButton from '@/components/confirmation-button'; import ProcessImportButton from '@/components/process-import'; -import { Process, ProcessMetadata } from '@/lib/data/process-schema'; +import { ProcessMetadata } from '@/lib/data/process-schema'; import MetaDataContent from '@/components/process-info-card-content'; import { useEnvironment } from '@/components/auth-can'; import { Folder } from '@/lib/data/folder-schema'; @@ -89,6 +87,7 @@ import { ContextActions, RowActions } from './types'; import { canDoActionOnResource } from './helpers'; import { useInitialisePotentialOwnerStore } from '@/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/use-potentialOwner-store'; import { useSession } from 'next-auth/react'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; // TODO: improve ordering export type ProcessActions = { @@ -401,6 +400,13 @@ const Processes = ({ spaceId: space.spaceId, userId: user?.id!, }); + if (isUserErrorResponse(existsResults)) { + const errorMessage = existsResults.error.message; + throw new Error( + typeof errorMessage === 'string' ? errorMessage : 'Something went wrong', + ); + } + existsResults.forEach((exists, idx) => { if (exists) { throw new Error( diff --git a/src/management-system-v2/lib/auth-database-adapter.ts b/src/management-system-v2/lib/auth-database-adapter.ts index d79cd5331..fb5e4e4fe 100644 --- a/src/management-system-v2/lib/auth-database-adapter.ts +++ b/src/management-system-v2/lib/auth-database-adapter.ts @@ -44,7 +44,12 @@ const Adapter = { try { // next-auth checks if the token is expired const token = await deleteEmailVerificationToken(params); - if (token.type === 'signin_with_email' || token.type === 'register_new_user') return token; + if (token.isErr()) { + throw token; + } + + if (token.value.type === 'signin_with_email' || token.value.type === 'register_new_user') + return token; else return null; } catch (_) { return null; @@ -63,10 +68,13 @@ const Adapter = { account.provider, account.providerAccountId, ); + if (userAccount.isErr()) { + return userAccount; + } - if (!userAccount) return null; + if (!userAccount.value) return null; - return getUserById(userAccount.userId) as unknown as AdapterAccount; + return getUserById(userAccount.value.userId) as unknown as AdapterAccount; }, }; diff --git a/src/management-system-v2/lib/auth.ts b/src/management-system-v2/lib/auth.ts index 660452a49..5568bfccf 100644 --- a/src/management-system-v2/lib/auth.ts +++ b/src/management-system-v2/lib/auth.ts @@ -58,7 +58,14 @@ const nextAuthOptions: NextAuthConfig = { let user = _user as User | undefined; - if (trigger === 'update') user = (await getUserById(token.user.id)) as User; + if (trigger === 'update') { + const newUserData = await getUserById(token.user.id); + if (newUserData.isErr()) { + throw newUserData.error; + } + + user = newUserData.value as User; + } if (user) token.user = user; @@ -86,7 +93,11 @@ const nextAuthOptions: NextAuthConfig = { ) { // Check if the user's cookie is correct const sessionUserInDb = await getUserById(sessionUser.id); - if (!sessionUserInDb || !sessionUserInDb.isGuest) throw new Error('Something went wrong'); + if (sessionUserInDb.isErr()) { + throw sessionUserInDb.error; + } + if (!sessionUserInDb || !sessionUserInDb.value.isGuest) + throw new Error('Something went wrong'); const userSigningIn = _user.id ? await getUserById(_user.id) : null; @@ -114,13 +125,16 @@ const nextAuthOptions: NextAuthConfig = { if (!token.user.isGuest) return; const user = await getUserById(token.user.id); + if (user.isErr()) { + throw user.error; + } if (user) { - if (!user.isGuest) { + if (!user.value.isGuest) { console.warn('User with invalid session'); return; } - await deleteUser(user.id); + await deleteUser(user.value.id); } }, async session({ session }) { @@ -226,7 +240,11 @@ if (env.PROCEED_PUBLIC_IAM_PERSONAL_SPACES_ACTIVE) { id: 'guest-signin', credentials: {}, async authorize() { - return addUser({ isGuest: true }); + const result = await addUser({ isGuest: true }); + if (result.isErr()) { + throw result.error; + } + return result.value; }, }), ); @@ -257,16 +275,19 @@ if (env.NODE_ENV === 'development') { }, }, async authorize(credentials) { - let user: User | null = null; + let user; if (credentials.username === 'johndoe') { - user = await getUserByUsername('johndoe'); + let user = await getUserByUsername('johndoe'); if (!user) user = await addUser(johnDoeTemplate); } else if (credentials.username === 'admin') { user = await getUserByUsername('admin'); } - return user; + if (!user) return null; + if (user.isErr()) throw user.error; + + return user.value; }, }), ); @@ -307,16 +328,19 @@ if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE) { }, authorize: async (credentials, req) => { const userAndPassword = await getUserAndPasswordByUsername(credentials.username as string); + if (userAndPassword.isErr()) { + throw userAndPassword.error; + } - if (!userAndPassword) return null; + if (!userAndPassword.value) return null; const passwordIsCorrect = await comparePassword( credentials.password as string, - userAndPassword.passwordAccount.password, + userAndPassword.value.passwordAccount.password, ); if (!passwordIsCorrect) return null; - return userAndPassword as User; + return userAndPassword.value as User; }, }), ); @@ -421,7 +445,7 @@ if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE || env.PROCEED_PUBLIC_IAM_ } else { // Only password is enabled -> immediately create user await db.$transaction(async (tx) => { - user = await addUser( + const addUserResult = await addUser( { username: credentials.username, firstName: credentials.firstName, @@ -431,9 +455,12 @@ if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE || env.PROCEED_PUBLIC_IAM_ }, tx, ); + if (addUserResult.isErr()) throw addUserResult.error; + user = addUserResult.value; const hashedPassword = await hashPassword(credentials.password as string); - await setUserPassword(user.id, hashedPassword, tx); + const setUserResult = await setUserPassword(user.id, hashedPassword, tx); + if (setUserResult.isErr()) throw setUserResult.error; }); } diff --git a/src/management-system-v2/lib/authorization/authorization.ts b/src/management-system-v2/lib/authorization/authorization.ts index 2c250b1f7..b20a4d0fc 100644 --- a/src/management-system-v2/lib/authorization/authorization.ts +++ b/src/management-system-v2/lib/authorization/authorization.ts @@ -6,6 +6,7 @@ import { getFolders } from '../data/db/folders'; import { getEnvironmentById } from '../data/db/iam/environments'; import { getAppliedRolesForUser } from './organizationEnvironmentRolesHelper'; import { MSEnabledResources } from './globalRules'; +import { ok } from 'neverthrow'; type PackedRules = PackedRulesForUser['rules']; @@ -52,11 +53,11 @@ export async function getSpaceFolderTree(spaceId: string) { const tree: TreeMap = {}; const folders = await getFolders(spaceId); - for (const folder of folders) { + for (const folder of folders.value) { if (folder.parentId) tree[folder.id] = folder.parentId; } - return tree; + return ok(tree); } /** @@ -70,18 +71,25 @@ export async function getUserRules(userId: string, environmentId: string) { // cached rules aren't being correctly removed after roles are updated let userRules = undefined; - if (userRules) return userRules; + if (userRules) return ok(userRules); const space = (await getEnvironmentById(environmentId))!; + if (space.isErr()) { + return space; + } - if (!space.isOrganization) { - const { rules, expiration } = computeRulesForUser({ userId, space }); + if (!space.value.isOrganization) { + const { rules, expiration } = computeRulesForUser({ userId, space: space.value }); cacheRulesForUser(userId, environmentId, rules, expiration); - return rules; + return ok(rules); } - if (space.isActive) { + if (space.value.isActive) { const roles = await getAppliedRolesForUser(userId, environmentId); + if (roles.isErr()) { + return roles; + } + // TODO: get bough features from db const getPurhasedFeatures = (_: string) => []; @@ -90,18 +98,27 @@ export async function getUserRules(userId: string, environmentId: string) { MSEnabledResources.includes(resource as any), ); - const { rules, expiration } = computeRulesForUser({ userId, space, roles, purchasedResources }); + const { rules, expiration } = computeRulesForUser({ + userId, + space: space.value, + roles: roles.value, + purchasedResources, + }); cacheRulesForUser(userId, environmentId, rules, expiration); - return rules; + return ok(rules); } // Non active organization - return []; + return ok([]); } export async function getAbilityForUser(userId: string, environmentId: string) { const spaceFolderTree = await getSpaceFolderTree(environmentId); + const userRules = await getUserRules(userId, environmentId); + if (userRules.isErr()) { + return userRules; + } - return new Ability(userRules, environmentId, spaceFolderTree); + return ok(new Ability(userRules.value, environmentId, spaceFolderTree.value)); } diff --git a/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts b/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts index bbb5c978c..b569f585e 100644 --- a/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts +++ b/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts @@ -2,31 +2,45 @@ import { Role } from '../data/role-schema'; import { getRoleById, getRoles } from '../data/db/iam/roles'; import { isMember } from '../data/db/iam/memberships'; import { getRoleMappingByUserId } from '../data/db/iam/role-mappings'; +import { ok } from 'neverthrow'; /** Returns all roles that are applied to a user in a given organization environment */ -export async function getAppliedRolesForUser( - userId: string, - environmentId: string, -): Promise { +export async function getAppliedRolesForUser(userId: string, environmentId: string) { // enforces environment to be an organization if (!isMember(environmentId, userId)) throw new Error('User is not a member of this environment'); const environmentRoles = await getRoles(environmentId); + if (environmentRoles.isErr()) { + return environmentRoles; + } const userRoles: Role[] = []; - const guestRole = environmentRoles.find((role: any) => role.name === '@guest') as Role; + const guestRole = environmentRoles.value.find((role: any) => role.name === '@guest') as Role; userRoles.push(guestRole); - if (userId === '') return userRoles; + if (userId === '') return ok(userRoles); - const everyoneRole = environmentRoles.find( + const everyoneRole = environmentRoles.value.find( (role: any) => role.default && role.name === '@everyone', ) as Role; userRoles.push(everyoneRole); + const roleMappings = await getRoleMappingByUserId(userId, environmentId); - const mappedRoles = await Promise.all(roleMappings.map((mapping) => getRoleById(mapping.roleId))); - userRoles.push(...mappedRoles); + if (roleMappings.isErr()) { + return roleMappings; + } + + const roleResults = await Promise.all( + roleMappings.value.map((mapping) => getRoleById(mapping.roleId)), + ); + + for (const role of roleResults) { + if (role && role.isErr()) { + return role; + } + if (role.value) userRoles.push(role.value); + } - return userRoles.filter((e) => e !== undefined); + return ok(userRoles); } diff --git a/src/management-system-v2/lib/custom-links/client-state.tsx b/src/management-system-v2/lib/custom-links/client-state.tsx index 55f399624..64a01987c 100644 --- a/src/management-system-v2/lib/custom-links/client-state.tsx +++ b/src/management-system-v2/lib/custom-links/client-state.tsx @@ -5,6 +5,7 @@ import { createContext, use } from 'react'; import { getCustomLinksStatus } from './server-actions'; import { Badge } from 'antd'; import { CustomNavigationLink } from './custom-link'; +import { isUserErrorResponse } from '../server-error-handling/user-error'; type LinkState = (CustomNavigationLink & { status?: boolean })[]; @@ -19,7 +20,11 @@ export function CustomLinkStateProvider({ }) { const { data } = useQuery({ queryKey: [spaceId, 'custom-links-state'], - queryFn: () => getCustomLinksStatus(spaceId), + queryFn: async () => { + const response = await getCustomLinksStatus(spaceId); + if (isUserErrorResponse(response)) throw response.error; + return response; + }, refetchInterval: 15_000, }); diff --git a/src/management-system-v2/lib/custom-links/server-actions.tsx b/src/management-system-v2/lib/custom-links/server-actions.tsx index 696d00fe2..e25446fe0 100644 --- a/src/management-system-v2/lib/custom-links/server-actions.tsx +++ b/src/management-system-v2/lib/custom-links/server-actions.tsx @@ -5,13 +5,19 @@ import { getSpaceSettingsValues } from '@/lib/data/db/space-settings'; import { asyncMap } from '../helpers/javascriptHelpers'; import { checkCustomLinkStatus } from './get-link-state'; import { CustomNavigationLink } from './custom-link'; +import { getErrorMessage, userError } from '../server-error-handling/user-error'; export async function getCustomLinksStatus(spaceId: string) { // Check that the user is a member of the space getCurrentEnvironment(spaceId); const generalSettings = await getSpaceSettingsValues(spaceId, 'general-settings'); - const customNavLinks: CustomNavigationLink[] = generalSettings.customNavigationLinks?.links || []; + if (generalSettings.isErr()) { + return userError(getErrorMessage(generalSettings.error)); + } + + const customNavLinks: CustomNavigationLink[] = + generalSettings.value.customNavigationLinks?.links || []; return await asyncMap(customNavLinks, async (link) => { return { diff --git a/src/management-system-v2/lib/data/db/folders.ts b/src/management-system-v2/lib/data/db/folders.ts index b2cab9181..1c4075eb8 100644 --- a/src/management-system-v2/lib/data/db/folders.ts +++ b/src/management-system-v2/lib/data/db/folders.ts @@ -104,7 +104,9 @@ export async function getFolderContents(folderId: string, ability?: Ability) { folderContent.push({ ...folder.value, type: 'folder' }); } - } catch (e) {} + } catch (e) { + return err(e); + } } return ok(folderContent); @@ -225,8 +227,10 @@ export async function updateFolderMetaData( folderId: string, newMetaDataInput: Partial, ability?: Ability, + tx?: Prisma.TransactionClient, ) { - const folder = await db.folder.findUnique({ + const mutator = tx || db; + const folder = await mutator.folder.findUnique({ where: { id: folderId }, }); @@ -242,7 +246,7 @@ export async function updateFolderMetaData( return err(new Error('environmentId cannot be changed')); } - const updatedFolder = await db.folder.update({ + const updatedFolder = await mutator.folder.update({ where: { id: folderId }, data: { ...newMetaDataInput, lastEditedOn: new Date() }, }); @@ -281,8 +285,15 @@ async function isInSubtree(rootId: string, nodeId: string): Promise ({ id })) } }); + return ok(); } /** Returns the html form html */ diff --git a/src/management-system-v2/lib/data/db/process.ts b/src/management-system-v2/lib/data/db/process.ts index 646daea70..deff5bb13 100644 --- a/src/management-system-v2/lib/data/db/process.ts +++ b/src/management-system-v2/lib/data/db/process.ts @@ -1,4 +1,4 @@ -import { ok, err } from 'neverthrow'; +import { ok, err, Result } from 'neverthrow'; import { getFolderById } from './folders'; import eventHandler from '../legacy/eventHandler.js'; import logger from '../legacy/logging.js'; @@ -9,7 +9,7 @@ import { transformBpmnAttributes, } from '../../helpers/processHelpers'; import { getDefinitionsVersionInformation, generateBpmnId } from '@proceed/bpmn-helper'; -import Ability from '@/lib/ability/abilityHelper'; +import Ability, { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { ProcessMetadata, ProcessServerInput, ProcessServerInputSchema } from '../process-schema'; import { getRootFolder } from './folders'; import { toCaslResource } from '@/lib/ability/caslAbility'; @@ -107,10 +107,18 @@ export async function getProcess(processDefinitionsId: string, includeBPMN = fal // : version.versionBasedOn, // })); + let bpmn; + if (includeBPMN) { + const result = await getProcessBpmn(processDefinitionsId); + if (result.isErr()) return result; + + bpmn = result.value; + } + const convertedProcess = { ...process, //versions: convertedVersions, - bpmn: includeBPMN ? await getProcessBpmn(processDefinitionsId) : null, + bpmn, shareTimestamp: typeof process.shareTimestamp === 'bigint' ? Number(process.shareTimestamp) @@ -631,7 +639,7 @@ export async function getProcessVersionBpmn(processDefinitionsId: string, versio if (existingProcess.isErr()) { return existingProcess; } - if (!existingProcess) { + if (!existingProcess.value) { return err(new Error('The process for which you try to get a version does not exist')); } const existingVersion = existingProcess.value.versions?.find( @@ -657,7 +665,7 @@ function removeExcessiveInformation(processInfo: Omit) } /** Returns the process definition for the process with the given id */ -export async function getProcessBpmn(processDefinitionsId: string) { +export async function getProcessBpmn(processDefinitionsId: string, ability?: Ability) { try { const process = await db.process.findUnique({ where: { @@ -672,6 +680,7 @@ export async function getProcessBpmn(processDefinitionsId: string) { id: true, name: true, originalId: true, + environmentId: true, }, }); @@ -679,6 +688,10 @@ export async function getProcessBpmn(processDefinitionsId: string) { return err(new Error('Process not found')); } + if (ability && !ability.can('view', toCaslResource('Process', process))) { + return err(UnauthorizedError); + } + const processWithStringDate = { ...process, createdOn: process.createdOn.toISOString(), @@ -743,7 +756,7 @@ export async function getProcessHtmlFormJSON( const jsonAsBuffer = (await retrieveFile(artifact.filePath, true)) as Buffer; return ok(jsonAsBuffer.toString('utf8')); } else { - return ok(); + return ok(undefined); } } catch (error) { logger.debug(`Error getting data of process html form ${fileName}. Reason\n${error}`); @@ -1176,7 +1189,7 @@ export async function copyProcessFiles(sourceProcessId: string, destinationProce } }); - return ok(oldNewFilenameMapping); + return Result.combine(oldNewFilenameMapping); } export async function getProcessImage(processDefinitionsId: string, imageFileName: string) { diff --git a/src/management-system-v2/lib/data/engines.ts b/src/management-system-v2/lib/data/engines.ts index 9a0810d77..20d2b763e 100644 --- a/src/management-system-v2/lib/data/engines.ts +++ b/src/management-system-v2/lib/data/engines.ts @@ -10,7 +10,7 @@ import { deleteSpaceEngine as _deleteDbEngine, } from '@/lib/data/db/engines'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; -import { UserErrorType, userError } from '../server-error-handling/user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { z } from 'zod'; import { enableUseDB } from 'FeatureFlags'; @@ -18,11 +18,26 @@ export async function getDbEngines(environmentId: string | null) { if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); let ability; - if (environmentId) ability = (await getCurrentEnvironment(environmentId)).ability; - const systemAdmin = (await getCurrentUser()).systemAdmin; + if (environmentId) { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) return userError(getErrorMessage(currentEnvironment.error)); + ability = currentEnvironment.value.ability; + } + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); try { - return await _getDbEngines(environmentId ?? null, ability, systemAdmin); + const result = await _getDbEngines( + environmentId ?? null, + ability, + currentUser.value.systemAdmin, + ); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -34,11 +49,27 @@ export async function getDbEngineById(engineId: string, environmentId: string | if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); let ability; - if (environmentId) ability = (await getCurrentEnvironment(environmentId)).ability; - const systemAdmin = (await getCurrentUser()).systemAdmin; + if (environmentId) { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) return userError(getErrorMessage(currentEnvironment.error)); + ability = currentEnvironment.value.ability; + } + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); try { - return await _getDbEngineById(engineId, environmentId ?? null, ability, systemAdmin); + const result = await _getDbEngineById( + engineId, + environmentId ?? null, + ability, + currentUser.value.systemAdmin, + ); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -50,11 +81,27 @@ export async function addDbEngines(enginesInput: SpaceEngineInput[], environment if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); let ability; - if (environmentId) ability = (await getCurrentEnvironment(environmentId)).ability; - const systemAdmin = (await getCurrentUser()).systemAdmin; + if (environmentId) { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) return userError(getErrorMessage(currentEnvironment.error)); + ability = currentEnvironment.value.ability; + } + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); try { - return await _addDbEngines(enginesInput, environmentId ?? null, ability, systemAdmin); + const result = await _addDbEngines( + enginesInput, + environmentId ?? null, + ability, + currentUser.value.systemAdmin, + ); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -72,17 +119,28 @@ export async function updateDbEngine( if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); let ability; - if (environmentId) ability = (await getCurrentEnvironment(environmentId)).ability; - const systemAdmin = (await getCurrentUser()).systemAdmin; + if (environmentId) { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) return userError(getErrorMessage(currentEnvironment.error)); + ability = currentEnvironment.value.ability; + } + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); try { - return await _updateDbEngine( + const result = await _updateDbEngine( engineId, engineInput, environmentId ?? null, ability, - systemAdmin, + currentUser.value.systemAdmin, ); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -96,11 +154,27 @@ export async function deleteSpaceEngine(engineId: string, environmentId: string if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); let ability; - if (environmentId) ability = (await getCurrentEnvironment(environmentId)).ability; - const systemAdmin = (await getCurrentUser()).systemAdmin; + if (environmentId) { + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) return userError(getErrorMessage(currentEnvironment.error)); + ability = currentEnvironment.value.ability; + } + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); try { - const result = await _deleteDbEngine(engineId, environmentId ?? null, ability, systemAdmin); + const result = await _deleteDbEngine( + engineId, + environmentId ?? null, + ability, + currentUser.value.systemAdmin, + ); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); diff --git a/src/management-system-v2/lib/data/environment-memberships.ts b/src/management-system-v2/lib/data/environment-memberships.ts index 498c1e694..a041a8718 100644 --- a/src/management-system-v2/lib/data/environment-memberships.ts +++ b/src/management-system-v2/lib/data/environment-memberships.ts @@ -22,6 +22,8 @@ import { env } from '../ms-config/env-vars'; import { AuthenticatedUser, AuthenticatedUserSchema, User } from './user-schema'; import { hashPassword } from '../password-hashes'; import db from '@/lib/data/db'; +import { Err, Ok, Result, err, ok } from 'neverthrow'; +import { Role } from './role-schema'; const EmailListSchema = z.array( z.union([z.object({ email: z.string().email() }), z.object({ username: z.string() })]), @@ -38,9 +40,15 @@ export async function inviteUsersToEnvironment( try { const invitedEmails = EmailListSchema.parse(invitedUsersIdentifiers); - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; - const organization = (await getEnvironmentById(environmentId)) as OrganizationEnvironment; + const _organization = await getEnvironmentById(environmentId); + if (_organization.isErr()) return userError(getErrorMessage(_organization.error)); + const organization = _organization.value as OrganizationEnvironment; // ability check disallows from adding to personal environments if (!ability.can('create', 'User')) @@ -49,28 +57,53 @@ export async function inviteUsersToEnvironment( UserErrorType.PermissionError, ); - const filteredRoles = roleIds?.filter(async (roleId) => { - return ( - ability.can('admin', 'All') && - ability.can('manage', toCaslResource('Role', await getRoleById(roleId))) && - ability.can( - 'create', - toCaslResource('RoleMapping', { userId: '', roleId } satisfies Partial), - { environmentId }, - ) + let filteredRoles; + if (roleIds) { + const allowedRolesResults = await Promise.all( + roleIds!.map(async (roleId) => { + const role = await getRoleById(roleId); + if (role.isErr()) return role; + if (!role.value) return err(); + + if ( + ability.can('admin', 'All') && + ability.can('manage', toCaslResource('Role', role.value)) && + ability.can( + 'create', + toCaslResource('RoleMapping', { + userId: '', + roleId, + } satisfies Partial), + { environmentId }, + ) + ) { + return ok(role.value.id); + } else { + return err(); + } + }), ); - }); + + filteredRoles = (allowedRolesResults.filter((result) => result.isOk()) as any[]).map( + (result) => result.value, + ) as string[]; + } for (const invitedUserIdentifier of invitedEmails) { let invitedUser: User | null = null; let invitedUserEmail: string | null | undefined = null; if ('email' in invitedUserIdentifier) { - invitedUser = await getUserByEmail(invitedUserIdentifier.email); + const userResult = await getUserByEmail(invitedUserIdentifier.email); + if (userResult.isErr()) continue; + invitedUser = userResult.value; } else if ('username' in invitedUserIdentifier) { - invitedUser = await getUserByUsername(invitedUserIdentifier.username); - // If there is no user with the given username there is nothing we can do - if (!invitedUser) continue; + const userResult = await getUserByUsername(invitedUserIdentifier.username); + + // If there is no user there is nothing we can do + if (userResult.isErr() || !userResult.value) continue; + + invitedUser = userResult.value; } // NOTE: technically not possible as guests cannot have an email or username @@ -121,7 +154,11 @@ export async function removeUsersFromEnvironment(environmentId: string, userIdsI try { const userIds = z.array(z.string()).parse(userIdsInput); - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability, activeEnvironment } = currentEnvironment.value; // TODO refine ability check @@ -156,7 +193,11 @@ export async function createUserAndAddToOrganization( }: z.infer & { password: string; roles: string[] }, ) { try { - const { ability } = await getCurrentEnvironment(organizationId); + const currentEnvironment = await getCurrentEnvironment(organizationId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; // Check if the user is an admin if (!ability.can('admin', 'All')) { @@ -169,27 +210,39 @@ export async function createUserAndAddToOrganization( const userDataParsed = createUserDataSchema.parse(userDataInput); let user: AuthenticatedUser; - await db.$transaction(async (tx) => { - // no need to check the if the user has permissions to create the role mappings since it's - // he's an admin - user = (await addUser( - { ...userDataParsed, isGuest: false, emailVerifiedOn: null }, - tx, - )) as AuthenticatedUser; - const passwordHash = await hashPassword(password); - await setUserPassword(user.id, passwordHash, tx, true); - - await addMember(organizationId, user.id, ability, tx); - await addRoleMappings( - roles.map((roleId) => ({ - roleId, - environmentId: organizationId, - userId: user.id, - })), - ability, - tx, - ); - }); + try { + await db.$transaction(async (tx) => { + // no need to check the if the user has permissions to create the role mappings since it's + // he's an admin + const userResult = await addUser( + { ...userDataParsed, isGuest: false, emailVerifiedOn: null }, + tx, + ); + if (userResult.isErr()) throw userResult.error; + + user = userResult.value as AuthenticatedUser; + + const passwordHash = await hashPassword(password); + const passwordResult = await setUserPassword(user.id, passwordHash, tx, true); + if (passwordResult.isErr()) throw passwordResult.error; + + const memberResult = await addMember(organizationId, user.id, ability, tx); + if (memberResult?.isErr()) throw memberResult.error; + + const roleMappingsResult = await addRoleMappings( + roles.map((roleId) => ({ + roleId, + environmentId: organizationId, + userId: user.id, + })), + ability, + tx, + ); + if (roleMappingsResult?.isErr()) throw roleMappingsResult.error; + }); + } catch (error) { + return userError(getErrorMessage(error)); + } return user!; } catch (error) { diff --git a/src/management-system-v2/lib/data/environments.ts b/src/management-system-v2/lib/data/environments.ts index a7dbffdc3..a35c7bdbf 100644 --- a/src/management-system-v2/lib/data/environments.ts +++ b/src/management-system-v2/lib/data/environments.ts @@ -26,7 +26,11 @@ export async function addOrganizationEnvironment( UserErrorType.PermissionError, ); - const { userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; try { const environmentData = UserOrganizationEnvironmentInputSchema.parse(environmentInput); @@ -37,14 +41,11 @@ export async function addOrganizationEnvironment( isOrganization: true, ...environmentData, }); - - if (result.isOk()) { - result.value; - } if (result.isErr()) { - // Handle error - result.error; + return userError(getErrorMessage(result.error)); } + + return result.value; } catch (e) { console.error(e); return userError('Error adding environment'); @@ -61,14 +62,24 @@ export async function deleteOrganizationEnvironments(environmentIds: string[]) { try { for (const environmentId of environmentIds) { - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; const environment = await getEnvironmentById(environmentId); + if (environment.isErr()) { + return userError(getErrorMessage(environment.error)); + } - if (!environment?.isOrganization) + if (!environment.value?.isOrganization) return userError(`Environment ${environmentId} is not an organization environment`); - deleteEnvironment(environmentId, ability); + const deleteResult = await deleteEnvironment(environmentId, ability); + if (deleteResult?.isErr()) { + return userError(getErrorMessage(deleteResult.error)); + } } } catch (e) { if (e instanceof UnauthorizedError) @@ -87,9 +98,16 @@ export async function updateOrganization( data: Partial, ) { try { - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + + const result = await _updateOrganization(environmentId, data, ability); + if (result.isErr()) return userError(getErrorMessage(result.error)); - return _updateOrganization(environmentId, data, ability); + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError("You're not allowed to update this organization"); @@ -100,7 +118,11 @@ export async function updateOrganization( export async function leaveOrganization(spaceId: string) { try { - const { user } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { user } = currentUser.value; if (!user || user.isGuest) { return userError('You need to be signed in'); @@ -115,7 +137,10 @@ export async function leaveOrganization(spaceId: string) { throw new Error(); } - await removeMember(spaceId, user.id); + const result = await removeMember(spaceId, user.id); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (e) { console.error(e); let message; diff --git a/src/management-system-v2/lib/data/file-manager-facade.ts b/src/management-system-v2/lib/data/file-manager-facade.ts index e169a3f6c..31ec4010c 100644 --- a/src/management-system-v2/lib/data/file-manager-facade.ts +++ b/src/management-system-v2/lib/data/file-manager-facade.ts @@ -412,13 +412,15 @@ export async function updateFileDeletableStatus( export async function softDeleteProcessUserTask(processId: string, userTaskFilename: string) { try { const userTaskJson = await getProcessHtmlFormJSON(processId, userTaskFilename); - if (userTaskJson) { + if (userTaskJson.isErr()) throw userTaskJson.error; + + if (userTaskJson.value) { const artifactsPromises = []; artifactsPromises.push(getArtifactMetaData(`${userTaskFilename}.json`, false)); artifactsPromises.push(getArtifactMetaData(`${userTaskFilename}.html`, false)); - const referencedArtifactFilePaths = getUsedImagesFromJson(JSON.parse(userTaskJson)); + const referencedArtifactFilePaths = getUsedImagesFromJson(JSON.parse(userTaskJson.value)); for (const referencedArtifactFilePath of referencedArtifactFilePaths) { artifactsPromises.push(getArtifactMetaData(referencedArtifactFilePath, true)); } @@ -472,13 +474,15 @@ export async function softDeleteProcessScriptTask(processId: string, scriptTaskF export async function revertSoftDeleteProcessUserTask(processId: string, userTaskFilename: string) { try { const userTaskJson = await getProcessHtmlFormJSON(processId, userTaskFilename, true); - if (userTaskJson) { + if (userTaskJson.isErr()) throw userTaskJson.error; + + if (userTaskJson.value) { const artifactsPromises = []; artifactsPromises.push(getArtifactMetaData(`${userTaskFilename}.json`, false)); artifactsPromises.push(getArtifactMetaData(`${userTaskFilename}.html`, false)); - const referencedArtifactFilePaths = getUsedImagesFromJson(JSON.parse(userTaskJson)); + const referencedArtifactFilePaths = getUsedImagesFromJson(JSON.parse(userTaskJson.value)); for (const referencedArtifactFilePath of referencedArtifactFilePaths) { artifactsPromises.push(getArtifactMetaData(referencedArtifactFilePath, true)); } diff --git a/src/management-system-v2/lib/data/folders.ts b/src/management-system-v2/lib/data/folders.ts index 9ed8aca80..b65a67b18 100644 --- a/src/management-system-v2/lib/data/folders.ts +++ b/src/management-system-v2/lib/data/folders.ts @@ -1,9 +1,8 @@ 'use server'; -import * as util from 'util'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; import { FolderUserInput, FolderUserInputSchema } from './folder-schema'; -import { UserErrorType, userError } from '../server-error-handling/user-error'; -import { TreeMap, toCaslResource } from '../ability/caslAbility'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; +import { toCaslResource } from '../ability/caslAbility'; import Ability, { UnauthorizedError } from '../ability/abilityHelper'; @@ -19,18 +18,35 @@ import { moveProcess, } from '@/lib/data/db/folders'; import { Process } from './process-schema'; +import { ResultAsync, ok } from 'neverthrow'; export type FolderChildren = { id: string; type: 'folder' } | { id: string; type: Process['type'] }; export async function createFolder(folderInput: FolderUserInput) { try { const folder = FolderUserInputSchema.parse(folderInput); - const { ability } = await getCurrentEnvironment(folder.environmentId); - const { userId } = await getCurrentUser(); - if (!folder.parentId) folder.parentId = (await getRootFolder(folder.environmentId)).id; + const currentEnvironment = await getCurrentEnvironment(folder.environmentId); + if (currentEnvironment.isErr()) return userError(getErrorMessage(currentEnvironment.error)); + const { ability } = currentEnvironment.value; - await _createFolder({ ...folder, createdBy: userId }, ability); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); + const { userId } = currentUser.value; + + if (!folder.parentId) { + const rootFolder = await getRootFolder(folder.environmentId); + if (rootFolder.isErr()) { + return userError(getErrorMessage(rootFolder.error)); + } + + folder.parentId = rootFolder.value.id; + } + + const result = await _createFolder({ ...folder, createdBy: userId }, ability); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } } catch (e) { return userError("Couldn't create folder"); } @@ -41,24 +57,24 @@ export type FolderTreeNode = { children: FolderTreeNode[]; }; -export async function getSpaceFolderTree( - spaceId: string, - ability?: Ability, -): Promise { +export async function getSpaceFolderTree(spaceId: string, ability?: Ability) { //TODO: ability check const folders = await getFolders(spaceId); + if (folders.isErr()) { + return userError(getErrorMessage(folders.error)); + } const folderMap: Record = {}; // Initialize the folder map with empty children arrays and only id and name - for (const folder of folders) { + for (const folder of folders.value) { folderMap[folder.id] = { id: folder.id, name: folder.name, children: [] }; } const rootFolders: FolderTreeNode[] = []; - for (const folder of folders) { + for (const folder of folders.value) { if (folder.parentId) { const parent = folderMap[folder.parentId]; if (parent) { @@ -70,16 +86,23 @@ export async function getSpaceFolderTree( } } - return rootFolders; + return rootFolders as FolderTreeNode[]; } export async function moveIntoFolder(items: FolderChildren[], folderId: string) { const folder = await getFolderById(folderId); + if (folder.isErr()) { + return userError(getErrorMessage(folder.error)); + } if (!folder) return userError('Folder not found'); - const { ability } = await getCurrentEnvironment(folder.environmentId); + const currentEnvironment = await getCurrentEnvironment(folder.value.environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; - if (!ability.can('update', toCaslResource('Folder', folder))) + if (!ability.can('update', toCaslResource('Folder', folder.value))) return userError('Permission denied'); for (const item of items) { @@ -92,27 +115,49 @@ export async function moveIntoFolder(items: FolderChildren[], folderId: string) } export async function getFolder(environmentId: string, folderId?: string) { - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; let folder; if (!folderId) folder = await getRootFolder(environmentId, ability); else folder = await getFolderById(folderId); - if (folder && !ability.can('view', toCaslResource('Folder', folder))) + if (folder.isErr()) { + return userError(getErrorMessage(folder.error)); + } + + if (folder.value && !ability.can('view', toCaslResource('Folder', folder.value))) return userError('Permission denied'); - if (!folder) return userError('Folder not found'); + if (!folder.value) return userError('Folder not found'); - return folder; + return folder.value; } export async function getFolderContents(environmentId: string, folderId?: string) { - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + + const rootFolder = await getRootFolder(environmentId); + if (rootFolder.isErr()) { + return userError(getErrorMessage(rootFolder.error)); + } - if (!folderId) folderId = (await getRootFolder(environmentId)).id; + if (!folderId) folderId = rootFolder.value.id; try { - return _getFolderContent(folderId, ability); + const result = await _getFolderContent(folderId, ability); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } else { + return result.value; + } } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -128,9 +173,16 @@ export async function updateFolder( ) { try { const folder = await getFolderById(folderId); - if (!folder) return userError('Folder not found'); + if (folder.isErr()) { + return userError(getErrorMessage(folder.error)); + } + if (!folder.value) return userError('Folder not found'); - const { ability } = await getCurrentEnvironment(folder.environmentId); + const currentEnvironment = await getCurrentEnvironment(folder.value.environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; const folderUpdate = FolderUserInputSchema.partial().parse(folderInput); if (folderUpdate.parentId) return userError('Wrong method for moving folders'); @@ -146,7 +198,11 @@ export async function updateFolder( export async function deleteFolder(folderIds: string[], spaceId: string) { try { - const { ability } = await getCurrentEnvironment(spaceId); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; for (const folderId of folderIds) await _deleteFolder(folderId, ability); } catch (e) { diff --git a/src/management-system-v2/lib/data/html-forms.ts b/src/management-system-v2/lib/data/html-forms.ts index b2f3eccb8..1dbfa68f8 100644 --- a/src/management-system-v2/lib/data/html-forms.ts +++ b/src/management-system-v2/lib/data/html-forms.ts @@ -14,7 +14,12 @@ import { getCurrentUser } from '@/components/auth'; export const getHtmlForms = async (spaceId: string) => { try { - return await _getHtmlForms(spaceId); + const result = await _getHtmlForms(spaceId); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (err) { console.error(`Unable to get html forms from the database. Reason: ${err}`); return userError('Unable to get data of html forms.'); @@ -23,7 +28,12 @@ export const getHtmlForms = async (spaceId: string) => { export const getHtmlForm = async (formId: string) => { try { - return await _getHtmlForm(formId); + const result = await _getHtmlForm(formId); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (err) { console.error(`Unable to get html form (${formId}) from the database. Reason: ${err}`); if (err instanceof UserFacingError) { @@ -39,13 +49,22 @@ export const addHtmlForm = async ( ) => { try { const creationTime = new Date(); - const { userId } = await getCurrentUser(); - await _addHtmlForm({ + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; + + const result = await _addHtmlForm({ ...formData, createdOn: creationTime, lastEditedOn: creationTime, creatorId: userId, }); + + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } } catch (err) { console.error(`Unable to add html form to the database. Reason: ${err}`); return userError('Unable to add html form.'); @@ -54,7 +73,10 @@ export const addHtmlForm = async ( export const updateHtmlForm = async (formId: string, newData: Partial) => { try { - await _updateHtmlForm(formId, newData); + const result = await _updateHtmlForm(formId, newData); + if (result && result.isErr()) { + return userError(getErrorMessage(result.error)); + } } catch (err) { console.error(`Unable to update html form ${formId} in the database. Reason: ${err}`); if (err instanceof UserFacingError) { @@ -67,7 +89,10 @@ export const updateHtmlForm = async (formId: string, newData: Partial) export const removeHtmlForms = async (formIds: string[]) => { try { - await _removeHtmlForms(formIds); + const result = await _removeHtmlForms(formIds); + if (result && result.isErr()) { + return userError(getErrorMessage(result.error)); + } } catch (err) { console.error(`Unable to remove html forms from the database. Reason: ${err}`); if (err instanceof UserFacingError) { @@ -80,7 +105,12 @@ export const removeHtmlForms = async (formIds: string[]) => { export const getHtmlFormHtml = async (formId: string) => { try { - return await _getHtmlFormHtml(formId); + const result = await _getHtmlFormHtml(formId); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (err) { console.error(`Unable to get html form html data from the database. Reason: ${err}`); if (err instanceof UserFacingError) { diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index ab9668939..039b7aa05 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -17,7 +17,13 @@ import { updateBpmnCreatorAttributes, } from '@proceed/bpmn-helper'; import { createProcess, getFinalBpmn, updateFileNames } from '../helpers/processHelpers'; -import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; +import { + UserError, + UserErrorType, + getErrorMessage, + isUserErrorResponse, + userError, +} from '../server-error-handling/user-error'; import { areVersionsEqual, getLocalVersionBpmn, @@ -62,6 +68,10 @@ import { ProcessData } from '@/components/process-import'; import { saveProcessArtifact } from './file-manager-facade'; import { getRootFolder } from './db/folders'; import { truthyFilter } from '../typescript-utils'; +import { Result, ok } from 'neverthrow'; +import { isSupportedCountry } from 'libphonenumber-js'; + +// FIXME: Check abilities // Import necessary functions from processModule @@ -70,10 +80,18 @@ export const checkValidity = async ( operation: 'view' | 'update' | 'delete', spaceId: string, ) => { - const { ability } = await getCurrentEnvironment(spaceId); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; const process = await _getProcess(definitionId); - if (!process) { + if (process.isErr()) { + return userError(getErrorMessage(process.error)); + } + + if (!process.value) { return userError('A process with this id does not exist.', UserErrorType.NotFoundError); } @@ -89,7 +107,7 @@ export const checkValidity = async ( if ( !ability.can(operation, toCaslResource('Process', process), { - environmentId: process.environmentId, + environmentId: process.value.environmentId, }) ) { return userError(errorMessages[operation], UserErrorType.PermissionError); @@ -98,9 +116,10 @@ export const checkValidity = async ( const getBpmnVersion = async (definitionId: string, versionId?: string) => { const process = await _getProcess(definitionId); + if (process.isErr()) return userError(getErrorMessage(process.error)); if (versionId) { - const version = process.versions.find((version) => version.id === versionId); + const version = process.value.versions.find((version) => version.id === versionId); if (!version) { return userError( @@ -109,20 +128,29 @@ const getBpmnVersion = async (definitionId: string, versionId?: string) => { ); } - return await getProcessVersionBpmn(definitionId, versionId); + const result = await getProcessVersionBpmn(definitionId, versionId); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } else { - return await _getProcessBpmn(definitionId); + const result = await _getProcessBpmn(definitionId); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } }; export const getSharedProcessWithBpmn = async (definitionId: string, versionCreatedOn?: string) => { const processMetaObj = await _getProcess(definitionId); + if (processMetaObj.isErr()) { + return userError(getErrorMessage(processMetaObj.error)); + } - if (!processMetaObj) { + if (!processMetaObj.value) { return userError(`Process does not exist `); } - if (processMetaObj.shareTimestamp > 0 || processMetaObj.allowIframeTimestamp > 0) { + if (processMetaObj.value.shareTimestamp > 0 || processMetaObj.value.allowIframeTimestamp > 0) { const bpmn = await getBpmnVersion(definitionId, versionCreatedOn); // check if getBpmnVersion returned an error that should be shown to the user instead of the bpmn @@ -140,20 +168,24 @@ export const getSharedProcessWithBpmn = async (definitionId: string, versionCrea export const getProcess = async ( definitionId: string, spaceId: string, + // FIXME: This allows anyone to just skip authorization skipValidityCheck = false, ) => { if (!skipValidityCheck) { const error = await checkValidity(definitionId, 'view', spaceId); - if (error) return error; } + const result = await _getProcess(definitionId); - return result as Process; + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value as Process; }; export const getProcessBPMN = async (definitionId: string, spaceId: string, versionId?: string) => { const error = await checkValidity(definitionId, 'view', spaceId); - if (error) return error; return await getBpmnVersion(definitionId, versionId); @@ -165,7 +197,8 @@ export const deleteProcesses = async (definitionIds: string[], spaceId: string) if (error) return error; - await removeProcess(definitionId); + const result = await removeProcess(definitionId); + if (result && result.isErr()) return userError(getErrorMessage(result.error)); } }; @@ -181,8 +214,17 @@ export const addProcesses = async ( spaceId: string, generateNewId: boolean = false, ) => { - const { ability, activeEnvironment } = await getCurrentEnvironment(spaceId); - const { userId } = await getCurrentUser(); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability, activeEnvironment } = currentEnvironment.value; + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; const newProcesses: Process[] = []; @@ -195,8 +237,15 @@ export const addProcesses = async ( }); // if imported process has a id that is not present in system, we can use that - if (value.id && (await checkIfProcessExists(value.id, false))) { - generateNewId = true; + if (value.id) { + const processExists = await checkIfProcessExists(value.id, false); + if (processExists.isErr()) { + return userError(getErrorMessage(processExists.error)); + } + + if (processExists.value) { + generateNewId = true; + } } if (generateNewId) { @@ -223,12 +272,15 @@ export const addProcesses = async ( // bpmn prop gets deleted in addProcess() const process = await _addProcess({ ...newProcess, folderId: value.folderId }); + if (process.isErr()) { + return userError(getErrorMessage(process.error)); + } - if (typeof process !== 'object') { + if (typeof process.value !== 'object') { return userError('A process with this id does already exist'); } - newProcesses.push({ ...process, bpmn }); + newProcesses.push({ ...process.value, bpmn }); } return newProcesses; @@ -245,11 +297,14 @@ export const updateProcessShareInfo = async ( if (error) return error; - await _updateProcessMetaData(definitionsId, { + const result = await _updateProcessMetaData(definitionsId, { sharedAs: sharedAs, shareTimestamp: shareTimestamp, allowIframeTimestamp: allowIframeTimestamp, }); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } }; export const updateProcess = async ( @@ -266,9 +321,17 @@ export const updateProcess = async ( if (error) return error; // Either replace or update the old BPMN. - let newBpmn = bpmn ?? (await _getProcessBpmn(definitionsId)); + let newBpmn = bpmn; + if (bpmn === undefined) { + const result = await _getProcessBpmn(definitionsId); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + newBpmn = result.value; + } + if (description !== undefined) { - newBpmn = (await addDocumentation(newBpmn!, description)) as string; + const result = await addDocumentation(newBpmn!, description); + newBpmn = result as string; } if (name !== undefined) { newBpmn = (await setDefinitionsName(newBpmn!, name)) as string; @@ -297,10 +360,12 @@ export const updateProcessMetaData = async ( invalidate = false, ) => { const error = await checkValidity(definitionsId, 'update', spaceId); - if (error) return error; - await _updateProcessMetaData(definitionsId, metaChanges); + const result = await _updateProcessMetaData(definitionsId, metaChanges); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } if (invalidate) { revalidatePath(`/processes/editor/${definitionsId}`); @@ -382,10 +447,9 @@ export const importProcesses = async (processData: ProcessData[], spaceId: strin } else { const newFileName = generateStartFormFileName(); fileNameMapping['start-form'].set(filename, newFileName); - try { - await _saveProcessHtmlForm(process.id, newFileName, json, html); - } catch (error) { - console.log(`Error processing start form ${newFileName}:`, error); + const result = await _saveProcessHtmlForm(process.id, newFileName, json, html); + if (result.isErr()) { + console.log(`Error processing start form ${newFileName}:`, result.error); } } } @@ -403,10 +467,9 @@ export const importProcesses = async (processData: ProcessData[], spaceId: strin const newUserTaskFileName = fileNameMapping['user-tasks'].get(baseName) || generateUserTaskFileName(); fileNameMapping['user-tasks'].set(baseName, newUserTaskFileName); - try { - await _saveProcessHtmlForm(process.id, newUserTaskFileName, json, html); - } catch (error) { - console.error(`Error processing user task ${newUserTaskFileName}:`, error); + const result = await _saveProcessHtmlForm(process.id, newUserTaskFileName, json, html); + if (result.isErr()) { + console.error(`Error processing user task ${newUserTaskFileName}:`, result.error); } } } @@ -448,8 +511,18 @@ export const copyProcesses = async ( destinationfolderId?: string, referencedProcessId?: string, ) => { - const { ability, activeEnvironment } = await getCurrentEnvironment(spaceId); - const { userId } = await getCurrentUser(); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability, activeEnvironment } = currentEnvironment.value; + + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; + const copiedProcesses: Process[] = []; for (const copyProcess of processes) { @@ -459,9 +532,12 @@ export const copyProcesses = async ( const originalBpmn = copyProcess.originalVersion ? await getProcessVersionBpmn(copyProcess.originalId, copyProcess.originalVersion) : await _getProcessBpmn(copyProcess.originalId); + if (originalBpmn.isErr()) { + return userError(getErrorMessage(originalBpmn.error)); + } // TODO: Does createProcess() do the same as this function? - let newBpmn = await getFinalBpmn({ ...copyProcess, id: newId, bpmn: originalBpmn! }); + let newBpmn = await getFinalBpmn({ ...copyProcess, id: newId, bpmn: originalBpmn.value! }); // TODO: include variables in copy? const newProcess = { @@ -476,15 +552,20 @@ export const copyProcesses = async ( return userError('Not allowed to create this process', UserErrorType.PermissionError); } const process = await _addProcess(newProcess, referencedProcessId); + if (process.isErr()) { + return userError(getErrorMessage(process.error)); + } - if (typeof process !== 'object') { + if (typeof process.value !== 'object') { return userError('A process with this id does already exist'); } await copyProcessArtifactReferences(copyProcess.originalId, newProcess.definitionId); const copiedFiles = await copyProcessFiles(copyProcess.originalId, newProcess.definitionId); - const changesFileNames = copiedFiles + if (copiedFiles.isErr()) return userError(getErrorMessage(copiedFiles.error)); + + const changesFileNames = copiedFiles.value .filter(truthyFilter) .filter((file) => file.artifactType === 'html-forms' || file.artifactType === 'script-tasks') .map( @@ -494,7 +575,7 @@ export const copyProcesses = async ( newBpmn = await updateFileNames(newBpmn, changesFileNames); await _updateProcess(newProcess.definitionId, { bpmn: newBpmn }); - copiedProcesses.push({ ...process, bpmn: newBpmn }); + copiedProcesses.push({ ...process.value, bpmn: newBpmn }); } return copiedProcesses; @@ -506,20 +587,25 @@ export const processHasChangesSinceLastVersion = async (processId: string, space if (error) return error; const process = await _getProcess(processId, true); - if (!process) return userError('Process not found', UserErrorType.NotFoundError); + if (process.isErr()) return userError(getErrorMessage(process.error)); + if (!process.value) return userError('Process not found', UserErrorType.NotFoundError); - const bpmnObj = await toBpmnObject(process.bpmn!); - const { versionBasedOn, versionCreatedOn } = await getDefinitionsVersionInformation(bpmnObj); + const bpmnObj = await toBpmnObject(process.value.bpmn!); + const { versionBasedOn } = await getDefinitionsVersionInformation(bpmnObj); const versionedBpmn = await toBpmnXml(bpmnObj); // if the new version has no changes to the version it is based on don't create a new version and return the previous version const basedOnBPMN = versionBasedOn !== undefined - ? await getLocalVersionBpmn(process as Process, versionBasedOn) + ? await getLocalVersionBpmn(process.value as Process, versionBasedOn) : undefined; + if (basedOnBPMN && basedOnBPMN.isErr()) { + return userError(getErrorMessage(basedOnBPMN.error)); + } - const versionsAreEqual = basedOnBPMN && (await areVersionsEqual(versionedBpmn, basedOnBPMN)); + const versionsAreEqual = + basedOnBPMN && (await areVersionsEqual(versionedBpmn, basedOnBPMN?.value)); return !versionsAreEqual; }; @@ -530,14 +616,13 @@ export const createVersion = async ( spaceId: string, ) => { const error = await checkValidity(processId, 'update', spaceId); - if (error) return error; const bpmn = await _getProcessBpmn(processId); - if (!bpmn) { - return null; - } - const bpmnObj = await toBpmnObject(bpmn); + if (bpmn.isErr()) return userError(getErrorMessage(bpmn.error)); + if (!bpmn) return null; + + const bpmnObj = await toBpmnObject(bpmn.value); const { versionBasedOn } = await getDefinitionsVersionInformation(bpmnObj); const versionCreatedOn = toCustomUTCString(new Date()); @@ -551,19 +636,30 @@ export const createVersion = async ( versionCreatedOn, }); - const process = (await _getProcess(processId)) as Process; + const _process = await _getProcess(processId); + if (_process.isErr()) return userError(getErrorMessage(_process.error)); + const process = _process.value as Process; const versionedProcessStartFormFilenames = await versionStartForm(process, versionId, bpmnObj); + if (isUserErrorResponse(versionedProcessStartFormFilenames)) + return versionedProcessStartFormFilenames; + const versionedUserTaskFilenames = await versionUserTasks(process, versionId, bpmnObj); + if (isUserErrorResponse(versionedUserTaskFilenames)) return versionedUserTaskFilenames; + const versionedScriptTaskFilenames = await versionScriptTasks(process, versionId, bpmnObj); + if (isUserErrorResponse(versionedScriptTaskFilenames)) return versionedScriptTaskFilenames; const versionedBpmn = await toBpmnXml(bpmnObj); // if the new version has no changes to the version it is based on don't create a new version and return the previous version const basedOnBPMN = versionBasedOn !== undefined ? await getLocalVersionBpmn(process, versionCreatedOn) : undefined; + if (basedOnBPMN && basedOnBPMN.isErr()) { + return userError(getErrorMessage(basedOnBPMN.error)); + } - if (basedOnBPMN && (await areVersionsEqual(versionedBpmn, basedOnBPMN))) { + if (basedOnBPMN && (await areVersionsEqual(versionedBpmn, basedOnBPMN?.value))) { return versionBasedOn; } @@ -576,7 +672,7 @@ export const createVersion = async ( versionedScriptTaskFilenames, ); - await updateProcessVersionBasedOn({ ...process, bpmn }, versionId); + await updateProcessVersionBasedOn({ ...process, bpmn: bpmn.value }, versionId); return versionId; }; @@ -605,7 +701,10 @@ export const getProcessHtmlFormData = async ( if (error) return error; try { - return await _getProcessHtmlFormJSON(definitionId, fileName); + const result = await _getProcessHtmlFormJSON(definitionId, fileName); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (err) { return userError( `Unable to get the requested process html form data for ${fileName}`, @@ -624,7 +723,10 @@ export const getProcessHtmlFormHTML = async ( if (error) return error; try { - return await _getHtmlForm(definitionId, fileName); + const result = await _getHtmlForm(definitionId, fileName); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (err) { return userError( `Unable to get the requested html form html ${fileName}.`, @@ -669,7 +771,13 @@ export const getProcessScriptTaskData = async ( if (error) return error; try { - return await _getProcessScriptTaskScript(definitionId, `${taskFileName}.${fileExtension}`); + const result = await _getProcessScriptTaskScript( + definitionId, + `${taskFileName}.${fileExtension}`, + ); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (err) { return userError('Unable to get the requested Script Task data.', UserErrorType.NotFoundError); } @@ -717,7 +825,10 @@ export const getProcessImage = async ( if (error) return error; - return _getProcessImage!(definitionId, imageFileName); + const result = await _getProcessImage!(definitionId, imageFileName); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; }; interface BaseProcessCheckData { @@ -738,35 +849,69 @@ interface BatchProcessCheckData extends BaseProcessCheckData { type ProcessCheckData = SingleProcessCheckData | BatchProcessCheckData; -export async function checkIfProcessExistsByName(data: SingleProcessCheckData): Promise; -export async function checkIfProcessExistsByName(data: BatchProcessCheckData): Promise; export async function checkIfProcessExistsByName( - data: ProcessCheckData, -): Promise { + data: SingleProcessCheckData, +): Promise; +export async function checkIfProcessExistsByName( + data: BatchProcessCheckData, +): Promise; +export async function checkIfProcessExistsByName(data: ProcessCheckData) { try { - const { ability } = await getCurrentEnvironment(data.spaceId); + const currentEnvironment = await getCurrentEnvironment(data.spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; if (data.batch === true) { - const processesWithFolderIds = await Promise.all( - data.processes.map(async (process) => ({ - name: process.name, - folderId: process.folderId ?? (await getRootFolder(data.spaceId, ability)).id, - })), + let rootFolderId: string | undefined = undefined; + const processesWithFolderIds = Result.combine( + await Promise.all( + data.processes.map(async (process) => { + let folderId = process.folderId; + + if (!folderId) { + if (!rootFolderId) { + const rootFolder = await getRootFolder(data.spaceId, ability); + if (rootFolder.isErr()) return rootFolder; + rootFolderId = rootFolder.value.id; + } + + folderId = rootFolderId; + } + return ok({ + name: process.name, + folderId: folderId, + }); + }), + ), ); + if (processesWithFolderIds.isErr()) { + return userError(getErrorMessage(processesWithFolderIds.error)); + } - return await checkIfProcessAlreadyExistsForAUserInASpaceByNameWithBatching( - processesWithFolderIds, + const result = await checkIfProcessAlreadyExistsForAUserInASpaceByNameWithBatching( + processesWithFolderIds.value, data.spaceId, data.userId, ); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } else { - const folderId = data.folderId ?? (await getRootFolder(data.spaceId, ability)).id; - return await checkIfProcessAlreadyExistsForAUserInASpaceByName( + const rootFolder = await getRootFolder(data.spaceId, ability); + if (rootFolder.isErr()) return userError(getErrorMessage(rootFolder.error)); + + const folderId = data.folderId ?? rootFolder.value.id; + const result = await checkIfProcessAlreadyExistsForAUserInASpaceByName( data.processName, data.spaceId, data.userId, folderId, ); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } } catch (error) { console.log(error); diff --git a/src/management-system-v2/lib/data/role-mappings.ts b/src/management-system-v2/lib/data/role-mappings.ts index 3466e4a94..4a72fcb4a 100644 --- a/src/management-system-v2/lib/data/role-mappings.ts +++ b/src/management-system-v2/lib/data/role-mappings.ts @@ -13,7 +13,11 @@ export async function addRoleMappings( roleMappings: Omit[], ) { try { - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability, activeEnvironment } = currentEnvironment.value; await _addRoleMappings( roleMappings.map((roleMapping) => ({ @@ -34,7 +38,11 @@ export async function deleteRoleMappings( ) { const errors: unknown[] = []; - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability, activeEnvironment } = currentEnvironment.value; for (const { userId, roleId } of roleMappings) { try { diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index ec04f34a7..aab9894d2 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -2,7 +2,7 @@ import { getCurrentEnvironment } from '@/components/auth'; import { redirect } from 'next/navigation'; -import { UserErrorType, userError } from '../server-error-handling/user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { RedirectType } from 'next/dist/client/components/redirect'; import { deleteRole, @@ -13,7 +13,11 @@ import { import { UnauthorizedError } from '../ability/abilityHelper'; export async function deleteRoles(envitonmentId: string, roleIds: string[]) { - const { ability } = await getCurrentEnvironment(envitonmentId); + const currentEnvironment = await getCurrentEnvironment(envitonmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; try { for (const roleId of roleIds) { @@ -27,14 +31,26 @@ export async function deleteRoles(envitonmentId: string, roleIds: string[]) { } export async function addRole(environmentId: string, role: Parameters[0]) { - const { activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { activeEnvironment } = currentEnvironment.value; let newRoleId; try { - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; const newRole = await _addRole({ ...role, environmentId: activeEnvironment.spaceId }, ability); - newRoleId = newRole.id; + if (newRole.isErr()) { + return userError(getErrorMessage(newRole.error)); + } + + newRoleId = newRole.value.id; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -50,12 +66,18 @@ export async function updateRole( updatedRole: Omit[1], 'environmentId'>, ) { try { - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); - await _updateRole( + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability, activeEnvironment } = currentEnvironment.value; + + const result = await _updateRole( roleId, { ...updatedRole, environmentId: activeEnvironment.spaceId }, ability, ); + if (result.isErr()) return userError(getErrorMessage(result.error)); } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -65,9 +87,18 @@ export async function updateRole( export async function getRoles(environmentId: string) { try { - const { ability } = await getCurrentEnvironment(environmentId); + const currentEnvironment = await getCurrentEnvironment(environmentId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + + const result = await _getRoles(environmentId, ability); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } - return await _getRoles(environmentId, ability); + return result.value; } catch (_) { return userError("Something wen't wrong"); } diff --git a/src/management-system-v2/lib/data/space-settings.ts b/src/management-system-v2/lib/data/space-settings.ts index 7b4764385..799d71209 100644 --- a/src/management-system-v2/lib/data/space-settings.ts +++ b/src/management-system-v2/lib/data/space-settings.ts @@ -13,8 +13,18 @@ import { UnauthorizedError } from '../ability/abilityHelper'; export async function getSpaceSettingsValues(spaceId: string, searchKey: string) { try { - const { ability } = await getCurrentEnvironment(spaceId); - return await _getSpaceSettingsValues(spaceId, searchKey, ability); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + + const result = await _getSpaceSettingsValues(spaceId, searchKey, ability); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('You do not have permission view settings', UserErrorType.PermissionError); @@ -24,8 +34,18 @@ export async function getSpaceSettingsValues(spaceId: string, searchKey: string) export async function populateSpaceSettingsGroup(spaceId: string, settingsGroup: SettingGroup) { try { - const { ability } = await getCurrentEnvironment(spaceId); - return await _populateSpaceSettingsGroup(spaceId, settingsGroup, ability); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + + const result = await _populateSpaceSettingsGroup(spaceId, settingsGroup, ability); + if (result && result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result?.value; } catch (e) { if (e instanceof UnauthorizedError) return userError( @@ -38,8 +58,18 @@ export async function populateSpaceSettingsGroup(spaceId: string, settingsGroup: export async function updateSpaceSettings(spaceId: string, data: Record) { try { - const { ability } = await getCurrentEnvironment(spaceId); - return await _updateSpaceSettings(spaceId, data, ability); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + + const result = await _updateSpaceSettings(spaceId, data, ability); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError( diff --git a/src/management-system-v2/lib/data/user-tasks.ts b/src/management-system-v2/lib/data/user-tasks.ts index d1fef27e7..464a11be4 100644 --- a/src/management-system-v2/lib/data/user-tasks.ts +++ b/src/management-system-v2/lib/data/user-tasks.ts @@ -10,18 +10,19 @@ import { deleteUserTask as _deleteUserTask, } from './db/user-tasks'; import { UnauthorizedError } from '../ability/abilityHelper'; -import { UserErrorType, userError } from '../server-error-handling/user-error'; +import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; import { UserTaskInput } from '../user-task-schema'; export async function getUserTasks() { if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); try { - return await _getUserTasks(); + const result = await _getUserTasks(); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (err) { - if (err instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error getting user tasks'); + return userError('Error getting user tasks'); } } @@ -29,11 +30,12 @@ export async function getUserTaskById(userTaskId: string) { if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); try { - return await _getUserTaskById(userTaskId); + const result = await _getUserTaskById(userTaskId); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (err) { - if (err instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error getting user tasks'); + return userError('Error getting user tasks'); } } @@ -41,7 +43,10 @@ export async function addUserTasks(userTasks: UserTaskInput[]) { if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); try { - return await _addUserTasks(userTasks); + const result = await _addUserTasks(userTasks); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (e) { if (e instanceof UnauthorizedError) return userError('Permission denied', UserErrorType.PermissionError); @@ -55,13 +60,12 @@ export async function updateUserTask(userTaskId: string, userTaskInput: Partial< if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); try { - return await _updateUserTask(userTaskId, userTaskInput); + const result = await _updateUserTask(userTaskId, userTaskInput); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else if (e instanceof z.ZodError) - return userError('Schema validation failed', UserErrorType.SchemaValidationError); - else return userError('Error getting updating user task'); + return userError('Error getting updating user task'); } } @@ -69,10 +73,11 @@ export async function deleteUserTask(userTaskId: string) { if (!enableUseDB) throw new Error('Not implemented for enableUseDB=false'); try { - return await _deleteUserTask(userTaskId); + const result = await _deleteUserTask(userTaskId); + if (result.isErr()) return userError(getErrorMessage(result.error)); + + return result.value; } catch (e) { - if (e instanceof UnauthorizedError) - return userError('Permission denied', UserErrorType.PermissionError); - else return userError('Error deleting user task'); + return userError('Error deleting user task'); } } diff --git a/src/management-system-v2/lib/data/users.tsx b/src/management-system-v2/lib/data/users.tsx index ee93aa6e9..8970b4628 100644 --- a/src/management-system-v2/lib/data/users.tsx +++ b/src/management-system-v2/lib/data/users.tsx @@ -1,10 +1,9 @@ 'use server'; import { getCurrentEnvironment, getCurrentUser } from '@/components/auth'; -import { UserErrorType, getErrorMessage, userError } from '../server-error-handling/user-error'; +import { UserErrorType, getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; import { AuthenticatedUserData, AuthenticatedUserDataSchema } from './user-schema'; import { ReactNode } from 'react'; -import { OrganizationEnvironment } from './environment-schema'; import Link from 'next/link'; import { UserHasToDeleteOrganizationsError, @@ -13,25 +12,35 @@ import { setUserPassword as _setUserPassword, getUserById, } from '@/lib/data/db/iam/users'; -import { getEnvironmentById } from './db/iam/environments'; import { hashPassword } from '../password-hashes'; import { getAppliedRolesForUser } from '../authorization/organizationEnvironmentRolesHelper'; import db from '@/lib/data/db/index'; import { env } from '../ms-config/env-vars'; export async function deleteUser() { - const { userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; + + const deleteResult = await _deleteUser(userId); + if (deleteResult.isOk()) return; try { - await _deleteUser(userId); - } catch (e) { + const e = deleteResult.error; let message: ReactNode; if (e instanceof UserHasToDeleteOrganizationsError) { - const conflictingOrgsNames = e.conflictingOrgs.map( - async (orgId: string) => - ((await getEnvironmentById(orgId)) as OrganizationEnvironment).name, - ); + const conflictingOrgs = await db.space.findMany({ + where: { + id: { in: e.conflictingOrgs }, + }, + select: { + name: true, + id: true, + }, + }); message = ( <> @@ -41,14 +50,9 @@ export async function deleteUser() {

The affected organizations are:

    - {conflictingOrgsNames.map((name, idx) => ( -
  • - {name}:{' '} - - manage roles here - + {conflictingOrgs.map(({ name, id }) => ( +
  • + {name}: manage roles here
  • ))}
@@ -62,19 +66,31 @@ export async function deleteUser() { } return userError(message); + } catch (_) { + return userError('Something weng wrong'); } } export async function updateUser(newUserDataInput: AuthenticatedUserData) { try { - const { userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; + const user = await getUserById(userId); + if (user.isErr()) { + return userError(getErrorMessage(user.error)); + } - if (user?.isGuest) { + if (user.value?.isGuest) { return userError('Guest users cannot be updated'); } - const newUserData = AuthenticatedUserDataSchema.parse(newUserDataInput); + const parseResult = AuthenticatedUserDataSchema.safeParse(newUserDataInput); + if (!parseResult.success) return userError('Malformed data'); + const newUserData = parseResult.data; await _updateUser(userId, { ...newUserData }); } catch (e) { @@ -83,32 +99,50 @@ export async function updateUser(newUserDataInput: AuthenticatedUserData) { } } -export async function getUsersFavourites(): Promise { - const { userId } = await getCurrentUser(); +export async function getUsersFavourites() { + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; const user = await getUserById(userId); + if (user.isErr()) { + return userError(getErrorMessage(user.error)); + } - if (user?.isGuest) { + if (user.value?.isGuest) { return []; // Guest users have no favourites } - return user?.favourites ?? []; + return user.value?.favourites ?? []; } export async function setUserPassword(newPassword: string) { try { - const { userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; + const user = await getUserById(userId); + if (user.isErr()) { + return userError(getErrorMessage(user.error)); + } - if (!user) { + if (!user.value) { return userError('Invalid session, please sign in again'); } - if (user?.isGuest) { + if (user.value?.isGuest) { return userError('Guest users cannot change their password'); } const passwordHash = await hashPassword(newPassword); - await _setUserPassword(userId, passwordHash); + const result = await _setUserPassword(userId, passwordHash); + if (result.isErr()) { + return userError(getErrorMessage(result.error)); + } } catch (e) { const message = getErrorMessage(e); return userError(message); @@ -124,15 +158,26 @@ export async function queryUsers(organizationId: string, searchQuery: string) { return userError('Unauthorized', UserErrorType.PermissionError); } - const { userId } = await getCurrentUser(); - const { activeEnvironment } = await getCurrentEnvironment(organizationId); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { userId } = currentUser.value; + + const currentEnvironment = await getCurrentEnvironment(organizationId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { activeEnvironment } = currentEnvironment.value; if (!activeEnvironment.isOrganization) { return userError('Unauthorized', UserErrorType.PermissionError); } const userRoles = await getAppliedRolesForUser(userId, organizationId); - const isAdmin = userRoles.some((role) => role.name === '@admin'); + if (userRoles.isErr()) return userError(getErrorMessage(userRoles.error)); + + const isAdmin = userRoles.value.some((role) => role.name === '@admin'); if (!isAdmin) { return userError('Unauthorized', UserErrorType.PermissionError); } @@ -175,7 +220,11 @@ export async function setUserTemporaryPassword( spaceId?: string, ) { try { - const { user, systemAdmin } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { user, systemAdmin } = currentUser.value; let allowed = false; if (systemAdmin) { diff --git a/src/management-system-v2/lib/db-seed.ts b/src/management-system-v2/lib/db-seed.ts index e1012e4df..33dcab129 100644 --- a/src/management-system-v2/lib/db-seed.ts +++ b/src/management-system-v2/lib/db-seed.ts @@ -5,7 +5,6 @@ import { addRoleMappings } from './data/db/iam/role-mappings'; import { OrganizationEnvironment, UserOrganizationEnvironmentInputSchema, - environmentSchema, } from './data/environment-schema'; import { addRole, getRoleById } from './data/db/iam/roles'; import { addEnvironment, getEnvironmentById } from './data/db/iam/environments'; @@ -166,6 +165,7 @@ function verifySeed(seed: DBSeed) { * -----------------------------------------------------------------------------------------------*/ async function writeSeedToDb(seed: DBSeed) { + // We need to throw inside the transcation to cancel it await db.$transaction(async (tx) => { const seedVersion = new Date(seed.version); const seedVersionDb = await tx.seedVersion.findUnique({ @@ -196,16 +196,20 @@ async function writeSeedToDb(seed: DBSeed) { const usernameToId = new Map(); for (const user of seed.users) { const existingUser = await getUserById(user.id); + if (existingUser.isErr()) throw existingUser.error; + if (existingUser) { // Use the username in seed-file instead of username in the db, as it may have changed - usernameToId.set(user.username, existingUser.id); + usernameToId.set(user.username, existingUser.value.id); continue; } const newUser = await addUser({ ...user, isGuest: false, emailVerifiedOn: null }, tx); + if (newUser.isErr()) throw newUser.error; + const hashedPassword = await hashPassword(user.initialPassword); await setUserPassword(user.id, hashedPassword, tx, true); - usernameToId.set(user.username, newUser.id); + usernameToId.set(user.username, newUser.value.id); } // Add system administrators @@ -235,9 +239,13 @@ async function writeSeedToDb(seed: DBSeed) { // Create / Update organizations for (const organization of seed.organizations) { // create org - let org = await getEnvironmentById(organization.id); + const existingOrg = await getEnvironmentById(organization.id); + if (existingOrg.isErr()) throw existingOrg.error; + + let org = existingOrg.value; + if (!org) { - org = (await addEnvironment( + const newOrg = await addEnvironment( { id: organization.id, ownerId: usernameToId.get(organization.owner)!, @@ -251,7 +259,10 @@ async function writeSeedToDb(seed: DBSeed) { }, undefined, tx, - )) as OrganizationEnvironment; + ); + if (newOrg.isErr()) throw newOrg.error; + + org = newOrg.value as OrganizationEnvironment; } // Add members + get their roles @@ -264,9 +275,11 @@ async function writeSeedToDb(seed: DBSeed) { // get members role mappings const userRoles = await getRoleMappingByUserId(memberId, org.id, undefined, undefined, tx); + if (userRoles.isErr()) throw userRoles.error; + userRoleMappings.set( memberId, - userRoles.map((role) => role.roleId), + userRoles.value.map((role) => role.roleId), ); } @@ -306,14 +319,17 @@ async function writeSeedToDb(seed: DBSeed) { const roleMembers = roleInput.members; delete (roleInput as any)['members']; - let role = await getRoleById(roleInput.id, undefined, tx); + const existingRole = await getRoleById(roleInput.id, undefined, tx); + if (existingRole.isErr()) throw existingRole.error; + + let role = existingRole.value; if (!role) { const rolePermissions: Partial> = {}; for (const [action, permissions] of Object.entries(roleInput.permissions)) { rolePermissions[action as ResourceType] = permissionIdentifiersToNumber(permissions); } - role = await addRole( + const newRole = await addRole( { ...roleInput, permissions: rolePermissions, @@ -322,6 +338,9 @@ async function writeSeedToDb(seed: DBSeed) { undefined, tx, ); + if (newRole.isErr()) throw newRole.error; + + role = newRole.value; } for (const roleMember of roleMembers) { diff --git a/src/management-system-v2/lib/email-verification-tokens/server-actions.ts b/src/management-system-v2/lib/email-verification-tokens/server-actions.ts index 3d8ae3836..b1f6a7737 100644 --- a/src/management-system-v2/lib/email-verification-tokens/server-actions.ts +++ b/src/management-system-v2/lib/email-verification-tokens/server-actions.ts @@ -1,7 +1,5 @@ 'use server'; - import { z } from 'zod'; -import { userError } from '../server-error-handling/user-error'; import { createChangeEmailVerificationToken, getTokenHash, notExpired } from './utils'; import { getCurrentUser } from '@/components/auth'; import { @@ -12,10 +10,16 @@ import { import { updateUser } from '@/lib/data/db/iam/users'; import { sendEmail } from '../email/mailer'; import renderSigninLinkEmail from '../email/signin-link-email'; +import { getErrorMessage, userError } from '../server-error-handling/user-error'; export async function requestEmailChange(newEmail: string) { try { - const { session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return userError(getErrorMessage(currentUser.error)); + } + const { session } = currentUser.value; + if (!session || session.user.isGuest) return userError('You must be signed in to change your email'); const userId = session.user.id; @@ -54,21 +58,32 @@ export async function requestEmailChange(newEmail: string) { } export async function changeEmail(token: string, identifier: string, cancel: boolean = false) { - const { session, userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return currentUser; + } + const { session, userId } = currentUser.value; if (!session || session.user.isGuest) return userError('You must be signed in to change your email'); const tokenParams = { identifier, token: await getTokenHash(token) }; const verificationToken = await getEmailVerificationToken(tokenParams); + if (verificationToken.isErr()) { + return userError(getErrorMessage(verificationToken.error)); + } + if ( !verificationToken || - verificationToken.type !== 'change_email' || - verificationToken.userId !== userId || - !(await notExpired(verificationToken)) + verificationToken.value?.type !== 'change_email' || + verificationToken.value?.userId !== userId || + !(await notExpired(verificationToken.value)) ) return userError('Invalid token'); - if (!cancel) updateUser(userId, { email: verificationToken.identifier, isGuest: false }); + if (!cancel) updateUser(userId, { email: verificationToken.value.identifier, isGuest: false }); - await deleteEmailVerificationToken(tokenParams); + const deleteResult = await deleteEmailVerificationToken(tokenParams); + if (deleteResult.isErr()) { + return userError(getErrorMessage(deleteResult.error)); + } } diff --git a/src/management-system-v2/lib/engines/server-actions.ts b/src/management-system-v2/lib/engines/server-actions.ts index baf890f1d..866019dc7 100644 --- a/src/management-system-v2/lib/engines/server-actions.ts +++ b/src/management-system-v2/lib/engines/server-actions.ts @@ -1,6 +1,11 @@ 'use server'; -import { UserFacingError, getErrorMessage, userError } from '../server-error-handling/user-error'; +import { + UserFacingError, + getErrorMessage, + isUserErrorResponse, + userError, +} from '../server-error-handling/user-error'; import { DeployedProcessInfo, deployProcess as _deployProcess, @@ -8,7 +13,7 @@ import { getProcessImageFromMachine, removeDeploymentFromMachines, } from './deployment'; -import { Engine, SpaceEngine } from './machines'; +import { Engine, ProceedEngine, SpaceEngine } from './machines'; import { savedEnginesToEngines } from './saved-engines-helpers'; import { getCurrentEnvironment } from '@/components/auth'; import { enableUseDB } from 'FeatureFlags'; @@ -49,18 +54,34 @@ export async function getCorrectTargetEngines( onlyProceedEngines = false, validatorFunc?: (engine: Engine) => Promise, ) { - const { ability } = await getCurrentEnvironment(spaceId); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; let engines: Engine[] = []; if (onlyProceedEngines) { // force that only proceed engines are supposed to be used const proceedSavedEngines = await getDbEngines(null, undefined, 'dont-check'); - engines = await savedEnginesToEngines(proceedSavedEngines); + if (proceedSavedEngines.isErr()) return userError(getErrorMessage(proceedSavedEngines.error)); + + engines = await savedEnginesToEngines(proceedSavedEngines.value); } else { // use all available engines const [proceedEngines, spaceEngines] = await Promise.allSettled([ - getDbEngines(null, undefined, 'dont-check').then(savedEnginesToEngines), - getDbEngines(spaceId, ability).then(savedEnginesToEngines), + (async () => { + const engines = await getDbEngines(null, undefined, 'dont-check'); + if (engines.isErr()) throw engines.error; + + return await savedEnginesToEngines(engines.value); + })(), + (async () => { + const engines = await getDbEngines(spaceId, ability); + if (engines.isErr()) throw engines.error; + + return await savedEnginesToEngines(engines.value); + })(), ]); if (proceedEngines.status === 'fulfilled') engines = proceedEngines.value; @@ -87,15 +108,24 @@ export async function deployProcess( let engines; if (_forceEngine && _forceEngine !== 'PROCEED') { // forcing a specific engine - const { ability } = await getCurrentEnvironment(spaceId); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + const address = _forceEngine.type === 'http' ? _forceEngine.address : _forceEngine.brokerAddress; + const spaceEngine = await getDbEngineByAddress(address, spaceId, ability); - if (!spaceEngine) throw new Error('No matching space engine found'); - engines = await savedEnginesToEngines([spaceEngine]); + if (spaceEngine.isErr()) return userError(getErrorMessage(spaceEngine.error)); + if (!spaceEngine.value) throw new Error('No matching space engine found'); + + engines = await savedEnginesToEngines([spaceEngine.value]); if (engines.length === 0) throw new Error("Engine couldn't be reached"); } else { engines = await getCorrectTargetEngines(spaceId, _forceEngine === 'PROCEED'); + if (isUserErrorResponse(engines)) return engines; } if (engines.length === 0) throw new UserFacingError('No fitting engine found.'); @@ -117,6 +147,8 @@ export async function removeDeployment(definitionId: string, spaceId: string) { return deployments.some((deployment) => deployment.definitionId === definitionId); }); + if (isUserErrorResponse(engines)) return engines; + await removeDeploymentFromMachines(engines, definitionId); } catch (e) { const message = getErrorMessage(e); @@ -130,6 +162,7 @@ export async function getAvailableTaskListEntries(spaceId: string) { throw new Error('getAvailableTaskListEntries only available with enableUseDB'); const engines = await getCorrectTargetEngines(spaceId); + if (isUserErrorResponse(engines)) return engines; let stored = await getUserTasks(); @@ -248,6 +281,8 @@ export async function getTasklistEntryHTML(spaceId: string, userTaskId: string, const definitionId = instanceId.split('-_')[0]; let engines = await getCorrectTargetEngines(spaceId); + if (isUserErrorResponse(engines)) return engines; + let deployments = await asyncMap(engines, async (engine) => { return [engine, await fetchDeployments([engine])] as [Engine, DeployedProcessInfo[]]; }); @@ -365,6 +400,8 @@ export async function addOwnerToTaskListEntry(spaceId: string, userTaskId: strin return instance.tokens.some((token) => token.currentFlowElementId === taskId); }); + if (isUserErrorResponse(engines)) return engines; + if (engines.length) { return await addOwnerToTaskListEntryOnMachine(engines[0], instanceId, taskId, owner); } @@ -414,6 +451,8 @@ export async function setTasklistEntryVariableValues( return instance.tokens.some((token) => token.currentFlowElementId === taskId); }); + if (isUserErrorResponse(engines)) return engines; + if (engines.length) { await setTasklistEntryVariableValuesOnMachine(engines[0], instanceId, taskId, variables); } @@ -461,6 +500,8 @@ export async function setTasklistMilestoneValues( return instance.tokens.some((token) => token.currentFlowElementId === taskId); }); + if (isUserErrorResponse(engines)) return engines; + if (engines.length) { await setTasklistEntryMilestoneValuesOnMachine(engines[0], instanceId, taskId, milestones); } @@ -506,6 +547,8 @@ export async function completeTasklistEntry( return instance.tokens.some((token) => token.currentFlowElementId === taskId); }); + if (isUserErrorResponse(engines)) return engines; + if (!engines.length) throw new Error('Failed to find the engine the user task is running on!'); @@ -555,6 +598,8 @@ export async function updateVariables( ); }); + if (isUserErrorResponse(engines)) return engines; + await asyncForEach( engines, async (engine) => await updateVariablesOnMachine(definitionId, instanceId, engine, variables), @@ -571,9 +616,16 @@ export async function getAvailableSpaceEngines(spaceId: string) { if (!enableUseDB) throw new Error('getAvailableEnginesForSpace only available with enableUseDB'); - const { ability } = await getCurrentEnvironment(spaceId); + const currentEnvironment = await getCurrentEnvironment(spaceId); + if (currentEnvironment.isErr()) { + return userError(getErrorMessage(currentEnvironment.error)); + } + const { ability } = currentEnvironment.value; + const spaceEngines = await getDbEngines(spaceId, ability); - return (await savedEnginesToEngines(spaceEngines)) as SpaceEngine[]; + if (spaceEngines.isErr()) return userError(getErrorMessage(spaceEngines.error)); + + return (await savedEnginesToEngines(spaceEngines.value)) as SpaceEngine[]; } catch (e) { const message = getErrorMessage(e); return userError(message); @@ -582,6 +634,7 @@ export async function getAvailableSpaceEngines(spaceId: string) { export async function getDeployment(spaceId: string, definitionId: string) { const engines = await getCorrectTargetEngines(spaceId); + if (isUserErrorResponse(engines)) return engines; const deployments = await fetchDeployments(engines); @@ -601,6 +654,8 @@ export async function getProcessImage(spaceId: string, definitionId: string, fil return deployments.some((deployment) => deployment.definitionId === definitionId); }); + if (isUserErrorResponse(engines)) return engines; + if (!engines.length) throw new Error('Failed to an engine the process was deployed to!'); return await getProcessImageFromMachine(engines[0], definitionId, fileName); diff --git a/src/management-system-v2/lib/engines/use-engines.tsx b/src/management-system-v2/lib/engines/use-engines.tsx index 4c8d8e7a4..a07cfa56d 100644 --- a/src/management-system-v2/lib/engines/use-engines.tsx +++ b/src/management-system-v2/lib/engines/use-engines.tsx @@ -1,17 +1,11 @@ import { useEnvironment } from '@/components/auth-can'; -import { - Engine, - HttpEngine, - MqttEngine, - SpaceEngine, - isHttpEngine, - isMqttEngine, -} from './machines'; +import { Engine, HttpEngine, MqttEngine, isHttpEngine, isMqttEngine } from './machines'; import { useCallback } from 'react'; import { getCorrectTargetEngines } from './server-actions'; import { useQuery } from '@tanstack/react-query'; import { asyncFilter } from '../helpers/javascriptHelpers'; import { truthyFilter } from '../typescript-utils'; +import { isUserErrorResponse } from '../server-error-handling/user-error'; function useEngines( filter: { key: any[]; fn: (engine: Engine) => Promise } = { @@ -24,6 +18,8 @@ function useEngines( const queryFn = useCallback(async () => { if (space.spaceId) { let res = await getCorrectTargetEngines(space.spaceId); + if (isUserErrorResponse(res)) throw res.error; + const knownEngines: Record = {}; res = await asyncFilter(res, filter.fn); diff --git a/src/management-system-v2/lib/helpers/processVersioning.ts b/src/management-system-v2/lib/helpers/processVersioning.ts index c70a60aa4..abe734aab 100644 --- a/src/management-system-v2/lib/helpers/processVersioning.ts +++ b/src/management-system-v2/lib/helpers/processVersioning.ts @@ -28,6 +28,7 @@ import { getHtmlForm, } from '@/lib/data/db/process'; import { getProcessHtmlFormHTML } from '../data/processes'; +import { getErrorMessage, userError } from '../server-error-handling/user-error'; const { diff } = require('bpmn-js-differ'); // TODO: This used to be a helper file in the old management system. It used @@ -159,12 +160,16 @@ export async function versionStartForm( if (!dryRun) { const startFormHtml = await getHtmlForm(processInfo.id, fileName); + if (startFormHtml.isErr()) return userError(getErrorMessage(startFormHtml.error)); + const startFormData = await getProcessHtmlFormJSON(processInfo.id, fileName); + if (startFormData.isErr()) return userError(getErrorMessage(startFormData.error)); + await saveProcessHtmlForm( processInfo.id, versionFileName, - startFormData!, - startFormHtml!, + startFormData.value!, + startFormHtml.value!, versionCreatedOn, ); } @@ -204,12 +209,16 @@ export async function versionUserTasks( if (!dryRun) { const userTaskHtml = await getHtmlForm(processInfo.id, fileName); + if (userTaskHtml.isErr()) return userError(getErrorMessage(userTaskHtml.error)); + const userTaskData = await getProcessHtmlFormJSON(processInfo.id, fileName); + if (userTaskData.isErr()) return userError(getErrorMessage(userTaskData.error)); + await saveProcessHtmlForm( processInfo.id, versionFileName, - userTaskData!, - userTaskHtml!, + userTaskData.value!, + userTaskHtml.value!, versionCreatedOn, ); } @@ -247,33 +256,36 @@ export async function versionScriptTasks( if (!dryRun) { try { const scriptTaskJS = await getProcessScriptTaskScript(processInfo.id, fileName + '.js'); + if (scriptTaskJS.isErr()) return userError(getErrorMessage(scriptTaskJS.error)); await saveProcessScriptTask( processInfo.id, versionFileName + '.js', - scriptTaskJS, + scriptTaskJS.value, versionCreatedOn, ); } catch (err) {} try { const scriptTaskTS = await getProcessScriptTaskScript(processInfo.id, fileName + '.ts'); + if (scriptTaskTS.isErr()) return userError(getErrorMessage(scriptTaskTS.error)); await saveProcessScriptTask( processInfo.id, versionFileName + '.ts', - scriptTaskTS, + scriptTaskTS.value, versionCreatedOn, ); } catch (err) {} try { const scriptTaskXML = await getProcessScriptTaskScript(processInfo.id, fileName + '.xml'); + if (scriptTaskXML.isErr()) return userError(getErrorMessage(scriptTaskXML.error)); await saveProcessScriptTask( processInfo.id, versionFileName + '.xml', - scriptTaskXML, + scriptTaskXML.value, versionCreatedOn, ); } catch (err) {} @@ -346,18 +358,20 @@ const getUsedStartFormFileNames = async (bpmn: string) => { }; export async function selectAsLatestVersion(processId: string, versionId: string) { - const versionBpmn = (await getProcessVersionBpmn(processId, versionId)) as string; + const versionBpmn = await getProcessVersionBpmn(processId, versionId); + if (versionBpmn.isErr()) return userError(getErrorMessage(versionBpmn.error)); const { bpmn: convertedBpmn, changedStartFormTaskFileNames, changedScriptTaskFileNames, changedUserTaskFileNames, - } = await convertToEditableBpmn(versionBpmn); + } = await convertToEditableBpmn(versionBpmn.value); - const editableBpmn = (await getProcessBpmn(processId)) as string; + const editableBpmn = await getProcessBpmn(processId); + if (editableBpmn.isErr()) return userError(getErrorMessage(editableBpmn.error)); - const startFormFileNameInEditableVersion = await getUsedStartFormFileNames(editableBpmn); + const startFormFileNameInEditableVersion = await getUsedStartFormFileNames(editableBpmn.value); await asyncForEach(startFormFileNameInEditableVersion, async (processFileName) => { await deleteHtmlForm(processId, processFileName); @@ -365,12 +379,16 @@ export async function selectAsLatestVersion(processId: string, versionId: string await asyncForEach(Object.entries(changedStartFormTaskFileNames), async ([oldName, newName]) => { const json = await getProcessHtmlFormJSON(processId, oldName); + if (json.isErr()) return; + const html = await getHtmlForm(processId, oldName); + if (html.isErr()) return; - if (json && html) await saveProcessHtmlForm(processId, newName, json, html); + if (json.value && html.value) + await saveProcessHtmlForm(processId, newName, json.value, html.value); }); - const scriptFileNamesinEditableVersion = await getUsedScriptTaskFileNames(editableBpmn); + const scriptFileNamesinEditableVersion = await getUsedScriptTaskFileNames(editableBpmn.value); // delete scripts stored for latest version await asyncForEach(scriptFileNamesinEditableVersion, async (taskFileName) => { @@ -384,12 +402,14 @@ export async function selectAsLatestVersion(processId: string, versionId: string for (const type of ['js', 'ts', 'xml']) { try { const fileContent = await getProcessScriptTaskScript(processId, oldName + '.' + type); - await saveProcessScriptTask(processId, newName + '.' + type, fileContent); + if (fileContent.isErr()) return; + + await saveProcessScriptTask(processId, newName + '.' + type, fileContent.value); } catch (err) {} } }); - const userTaskFileNamesinEditableVersion = await getUsedUserTaskFileNames(editableBpmn); + const userTaskFileNamesinEditableVersion = await getUsedUserTaskFileNames(editableBpmn.value); // Delete UserTasks stored for latest version await asyncForEach(userTaskFileNamesinEditableVersion, async (taskFileName) => { @@ -399,9 +419,13 @@ export async function selectAsLatestVersion(processId: string, versionId: string // Store UserTasks from this version as UserTasks from latest version await asyncForEach(Object.entries(changedUserTaskFileNames), async ([oldName, newName]) => { const json = await getProcessHtmlFormJSON(processId, oldName); + if (json.isErr()) return; + const html = await getHtmlForm(processId, oldName); + if (html.isErr()) return; - if (json && html) await saveProcessHtmlForm(processId, newName, json, html); + if (json.value && html.value) + await saveProcessHtmlForm(processId, newName, json.value, html.value); }); // Store bpmn from this version as latest version diff --git a/src/management-system-v2/lib/invitation-tokens.ts b/src/management-system-v2/lib/invitation-tokens.ts index e4d144213..87e83271a 100644 --- a/src/management-system-v2/lib/invitation-tokens.ts +++ b/src/management-system-v2/lib/invitation-tokens.ts @@ -8,6 +8,7 @@ import { addMember, isMember } from './data/db/iam/memberships'; import { getUserByEmail } from './data/db/iam/users'; import { getRoleById } from './data/db/iam/roles'; import { addRoleMappings } from './data/db/iam/role-mappings'; +import { getErrorMessage, userError } from './server-error-handling/user-error'; const baseInvitationSchema = { spaceId: z.string(), @@ -40,34 +41,52 @@ export function getInvitation(token: string) { } export async function acceptInvitation(invite: Invitation, userIdAcceptingInvite?: string) { - const organization = await getEnvironmentById(invite.spaceId); + const _organization = await getEnvironmentById(invite.spaceId); + if (_organization.isErr()) return userError(getErrorMessage(_organization.error)); + const organization = _organization.value; + if (!organization || !organization.isOrganization || !organization.isActive) return { error: 'InvalidOrganization' as const }; - const userId = 'userId' in invite ? invite.userId : (await getUserByEmail(invite.email))?.id; + let userId; + if ('userId' in invite) { + userId = invite.userId; + } else { + const userByEmail = await getUserByEmail(invite.email); + if (userByEmail.isErr()) return userError(getErrorMessage(userByEmail.error)); + if (!userByEmail.value) return userError('User not found'); + + userId = userByEmail.value.id; + } if (!userId) return { error: 'UserNotFound' as const }; if (userIdAcceptingInvite && userIdAcceptingInvite !== userId) return { error: 'WrongUser' as const }; - if (!(await isMember(invite.spaceId, userId))) { - addMember(invite.spaceId, userId); + const userIsMember = await isMember(invite.spaceId, userId); + if (userIsMember.isErr()) return userError(getErrorMessage(userIsMember.error)); + + if (!userIsMember.value) { + const memberAdded = await addMember(invite.spaceId, userId); + if (memberAdded?.isErr()) return userError(getErrorMessage(memberAdded.error)); if (invite.roleIds) { const validRoles = []; for (const roleId of invite.roleIds) { // skip roles that have been deleted - if (await getRoleById(roleId)) validRoles.push(roleId); + const role = await getRoleById(roleId); + if (role.isOk() && role.value) validRoles.push(roleId); } - await addRoleMappings( + const result = await addRoleMappings( validRoles.map((roleId) => ({ environmentId: invite.spaceId, roleId, userId, })), ); + if (result?.isErr()) return userError(getErrorMessage(result.error)); } } } diff --git a/src/management-system-v2/lib/page-error-handling.tsx b/src/management-system-v2/lib/page-error-handling.tsx deleted file mode 100644 index 2439b829d..000000000 --- a/src/management-system-v2/lib/page-error-handling.tsx +++ /dev/null @@ -1,8 +0,0 @@ -import { Err, type Result } from 'neverthrow'; -import { UserFacingError } from './server-error-handling/user-error'; -import { UnauthorizedError } from './ability/abilityHelper'; -import { Err } from './errors'; - -export function errorResponse( - result: Err, -) {} diff --git a/src/management-system-v2/lib/server-error-handling/page-error-response.tsx b/src/management-system-v2/lib/server-error-handling/page-error-response.tsx index d8ed5dfbd..7cd2ee2ca 100644 --- a/src/management-system-v2/lib/server-error-handling/page-error-response.tsx +++ b/src/management-system-v2/lib/server-error-handling/page-error-response.tsx @@ -7,9 +7,9 @@ import { UnauthorizedError } from '../ability/abilityHelper'; import RetryButton from './retry-button'; export function errorResponse( - result: Err, + result: Err | unknown, ) { - const error = result.error; + const error = result instanceof Err ? result.error : undefined; let title = 'Something Went wrong'; let status: ResultProps['status'] = 'warning'; diff --git a/src/management-system-v2/lib/sharing/process-sharing.ts b/src/management-system-v2/lib/sharing/process-sharing.ts index 23280a081..0049da881 100644 --- a/src/management-system-v2/lib/sharing/process-sharing.ts +++ b/src/management-system-v2/lib/sharing/process-sharing.ts @@ -76,16 +76,3 @@ export async function generateSharedViewerUrl( return userError('Something went wrong'); } } - -export async function getAllUserWorkspaces(userId: string, ability?: Ability) { - // if (ability && !ability.can('delete', 'Environment')) throw new UnauthorizedError(); - - const userEnvironments: Environment[] = [(await getEnvironmentById(userId))!]; - const userOrgEnvs = await getUserOrganizationEnvironments(userId); - const orgEnvironments = (await asyncMap(userOrgEnvs, (environmentId) => - getEnvironmentById(environmentId), - )) as Environment[]; - - userEnvironments.push(...orgEnvironments); - return userEnvironments; -} From 114b62732e9c62a0c073cae4c6f820e26781c55a Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 9 Dec 2025 02:16:05 +0100 Subject: [PATCH 04/43] fix --- .../[environmentId]/iam/users/page.tsx | 21 ++++--------------- 1 file changed, 4 insertions(+), 17 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx index 52e9d8cba..69ae53904 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx @@ -2,12 +2,8 @@ import { getCurrentEnvironment } from '@/components/auth'; import UsersPage from './users-page'; import Content from '@/components/content'; import UnauthorizedFallback from '@/components/unauthorized-fallback'; -import { getMembers } from '@/lib/data/db/iam/memberships'; -import { getUserById } from '@/lib/data/db/iam/users'; -import { AuthenticatedUser } from '@/lib/data/user-schema'; -import { asyncMap } from '@/lib/helpers/javascriptHelpers'; +import { getFullMembersWithRoles } from '@/lib/data/db/iam/memberships'; import { errorResponse } from '@/lib/server-error-handling/page-error-response'; -import { Result } from 'neverthrow'; const Page = async ({ params }: { params: { environmentId: string } }) => { const currentSpace = await getCurrentEnvironment(params.environmentId); @@ -17,21 +13,12 @@ const Page = async ({ params }: { params: { environmentId: string } }) => { const { ability, activeEnvironment } = currentSpace.value; if (!ability.can('manage', 'User')) return ; - const memberships = await getMembers(activeEnvironment.spaceId, ability); - if (memberships.isErr()) { - return errorResponse(memberships); - } - - const users = Result.combine( - await asyncMap(memberships.value, (user) => getUserById(user.userId)), - ); - if (users.isErr()) { - return errorResponse(users); - } + const users = await getFullMembersWithRoles(activeEnvironment.spaceId, ability); + if (users.isErr()) return errorResponse(users); return ( - + ); }; From 6563262da4e55238b45d12cc87f01ba5e3ffb31e Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 9 Dec 2025 10:05:23 +0100 Subject: [PATCH 05/43] fixes --- src/management-system-v2/lib/data/db/iam/memberships.ts | 5 +++-- src/management-system-v2/lib/engines/instances.ts | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/management-system-v2/lib/data/db/iam/memberships.ts b/src/management-system-v2/lib/data/db/iam/memberships.ts index 7ccc17a1f..af358b94a 100644 --- a/src/management-system-v2/lib/data/db/iam/memberships.ts +++ b/src/management-system-v2/lib/data/db/iam/memberships.ts @@ -60,7 +60,7 @@ export async function getMembers(environmentId: string, ability?: Ability) { } export async function getFullMembersWithRoles(environmentId: string, ability?: Ability) { - if (ability && !ability.can('admin', 'User')) throw new UnauthorizedError(); + if (ability && !ability.can('admin', 'User')) err(new UnauthorizedError()); const usersWithRoles = await db.user.findMany({ where: { @@ -95,7 +95,8 @@ export async function getFullMembersWithRoles(environmentId: string, ability?: A roles: User['roleMembers'][number]['role'][]; isGuest: false; }; - return usersWithRoles as unknown as TransformedUserType[]; + + return ok(usersWithRoles as unknown as TransformedUserType[]); } /** diff --git a/src/management-system-v2/lib/engines/instances.ts b/src/management-system-v2/lib/engines/instances.ts index 4910ca2fb..c8058651a 100644 --- a/src/management-system-v2/lib/engines/instances.ts +++ b/src/management-system-v2/lib/engines/instances.ts @@ -2,7 +2,7 @@ import { Engine } from './machines'; import { engineRequest } from './endpoints/index'; -import { userError } from '../user-error'; +import { userError } from '@/lib/server-error-handling/user-error'; export async function startInstanceOnMachine( definitionId: string, From f53390667060235b942bb52d9f97fce384397cf2 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 9 Dec 2025 13:09:48 +0100 Subject: [PATCH 06/43] fix: error type checks --- src/management-system-v2/lib/data/db/iam/users.ts | 8 ++++++-- src/management-system-v2/lib/db-seed.ts | 4 +++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/management-system-v2/lib/data/db/iam/users.ts b/src/management-system-v2/lib/data/db/iam/users.ts index a4c31edf3..f0ba17ff1 100644 --- a/src/management-system-v2/lib/data/db/iam/users.ts +++ b/src/management-system-v2/lib/data/db/iam/users.ts @@ -92,10 +92,14 @@ export async function _addUser( const [usernameRes, emailRes] = await Promise.all(checks); if (usernameRes) { - return err(new NextAuthUsernameTakenError()); + if (usernameRes.isErr()) return usernameRes; + + if (usernameRes.value) return err(new NextAuthUsernameTakenError()); } if (emailRes) { - return err(new NextAuthEmailTakenError()); + if (emailRes.isErr()) return emailRes; + + if (emailRes.value) return err(new NextAuthEmailTakenError()); } } diff --git a/src/management-system-v2/lib/db-seed.ts b/src/management-system-v2/lib/db-seed.ts index 33dcab129..55ce88150 100644 --- a/src/management-system-v2/lib/db-seed.ts +++ b/src/management-system-v2/lib/db-seed.ts @@ -198,14 +198,16 @@ async function writeSeedToDb(seed: DBSeed) { const existingUser = await getUserById(user.id); if (existingUser.isErr()) throw existingUser.error; - if (existingUser) { + if (existingUser.value) { // Use the username in seed-file instead of username in the db, as it may have changed usernameToId.set(user.username, existingUser.value.id); continue; } + console.log('1'); const newUser = await addUser({ ...user, isGuest: false, emailVerifiedOn: null }, tx); if (newUser.isErr()) throw newUser.error; + console.log('2'); const hashedPassword = await hashPassword(user.initialPassword); await setUserPassword(user.id, hashedPassword, tx, true); From a39e94c87905b6cfa0c78c3214d9d5af5ded6ed4 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Tue, 9 Dec 2025 17:56:16 +0100 Subject: [PATCH 07/43] fix(ms2): error handling get process with bpmn --- .../app/shared-viewer/page.tsx | 89 +++++++++++-------- .../components/error-message.tsx | 3 +- .../lib/data/processes.tsx | 2 +- 3 files changed, 53 insertions(+), 41 deletions(-) diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index d01fd1424..734aaf6c5 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -22,8 +22,15 @@ import { getDefinitionsAndProcessIdForEveryCallActivity } from '@proceed/bpmn-he import { SettingsOption } from './settings-modal'; import { asyncMap } from '@/lib/helpers/javascriptHelpers'; import { env } from '@/lib/ms-config/env-vars'; -import { getErrorMessage, userError } from '@/lib/server-error-handling/user-error'; +import { + UserError, + getErrorMessage, + isUserErrorResponse, + userError, +} from '@/lib/server-error-handling/user-error'; import { Result } from 'neverthrow'; +import { ReactNode } from 'react'; +import { undefined } from 'zod'; interface PageProps { searchParams: { @@ -49,27 +56,23 @@ const getProcessInfo = async ( versionId?: string, ) => { const currentUser = await getCurrentUser(); - if (currentUser.isErr()) { - return errorResponse(currentUser); - } - const { session, userId } = currentUser.value; + if (currentUser.isErr()) return userError(getErrorMessage(currentUser.error)); + const { session } = currentUser.value; + let spaceId; let isOwner = false; let processData; // check if there is a session (=> the user is already logged in) if (session) { const currentSpace = await getCurrentEnvironment(session?.user.id); - if (currentSpace.isErr()) { - return errorResponse(currentSpace); - } + if (currentSpace.isErr()) return userError(getErrorMessage(currentSpace.error)); + const { ability, activeEnvironment } = currentSpace.value; ({ spaceId } = activeEnvironment); // get all the processes the user has access to const ownedProcesses = await getProcesses(spaceId, ability); - if (ownedProcesses.isErr()) { - return userError(getErrorMessage(ownedProcesses.error)); - } + if (ownedProcesses.isErr()) return userError(getErrorMessage(ownedProcesses.error)); // check if the current user is the owner of the process(/has access to the process) => if yes give access regardless of sharing status isOwner = ownedProcesses.value.some((process) => process.id === definitionId); @@ -93,19 +96,17 @@ const getProcessInfo = async ( } else { // the user has no regular access to the process so get the process data from the sharing api const res = await getSharedProcessWithBpmn(definitionId, versionId); - if ('error' in res) { - return ; - } else { - processData = res; - - if ( - // bypass the timestamp check for imports - !isImport && - ((embeddedMode && timestamp !== processData.value.allowIframeTimestamp) || - (!embeddedMode && timestamp !== processData.value.shareTimestamp)) - ) { - return ; - } + if ('error' in res) return res; + + processData = res; + + if ( + // bypass the timestamp check for imports + !isImport && + ((embeddedMode && timestamp !== processData.allowIframeTimestamp) || + (!embeddedMode && timestamp !== processData.shareTimestamp)) + ) { + return userError('Token expired'); } } @@ -118,7 +119,10 @@ const getProcessInfo = async ( * @param bpmn the bpmn of the process to get the imports for * @param knownInfos the object to put the bpmns into */ -const getImportInfos = async (bpmn: string, knownInfos: ImportsInfo) => { +const getImportInfos = async ( + bpmn: string, + knownInfos: ImportsInfo, +): Promise<{ error: UserError } | undefined> => { // information which tasks reference which processes const taskImportMap = await getDefinitionsAndProcessIdForEveryCallActivity(bpmn, true); @@ -127,17 +131,15 @@ const getImportInfos = async (bpmn: string, knownInfos: ImportsInfo) => { if (!(knownInfos[definitionId] && knownInfos[definitionId][versionId])) { const processInfo = await getProcessInfo(definitionId, 0, false, true, versionId); + if (isUserErrorResponse(processInfo)) return processInfo; - // check if the return value is a valid process info (might also be a react component that signals an error => no isOwner) - if ('isOwner' in processInfo && processInfo.processData) { - const { bpmn: importBpmn } = processInfo.processData; + const { bpmn: importBpmn } = processInfo.processData; - if (!knownInfos[definitionId]) knownInfos[definitionId] = {}; - knownInfos[definitionId][versionId] = importBpmn as string; + if (!knownInfos[definitionId]) knownInfos[definitionId] = {}; + knownInfos[definitionId][versionId] = importBpmn as string; - // recursively get the imports of the imports - await getImportInfos(importBpmn as string, knownInfos); - } + // recursively get the imports of the imports + return await getImportInfos(importBpmn as string, knownInfos); } } }; @@ -191,10 +193,12 @@ const SharedViewer = async ({ searchParams }: PageProps) => { ); // the return value of getProcessInfo might be an error that should just be returned to the user - if (!('isOwner' in processInfo)) { - return processInfo; + if (isUserErrorResponse(processInfo)) { + return ; } + console.log('processData', processData); + ({ isOwner, processData } = processInfo); if (!processData) { @@ -208,10 +212,17 @@ const SharedViewer = async ({ searchParams }: PageProps) => { let availableImports: ImportsInfo = {}; if (!iframeMode) { + let errorMessage: ReactNode; try { - await getImportInfos(processData.bpmn, availableImports); - } catch (err) { - console.error('Failed to resolve the information for process imports: ', err); + const error = await getImportInfos(processData.bpmn, availableImports); + if (isUserErrorResponse(error)) errorMessage = error.error.message; + } catch (error) { + errorMessage = getErrorMessage(error); + console.error('Failed to resolve the information for process imports: ', error); + } + + if (errorMessage) { + return ; } } @@ -244,7 +255,7 @@ const SharedViewer = async ({ searchParams }: PageProps) => { diff --git a/src/management-system-v2/components/error-message.tsx b/src/management-system-v2/components/error-message.tsx index f25fa0b37..683ac6e0d 100644 --- a/src/management-system-v2/components/error-message.tsx +++ b/src/management-system-v2/components/error-message.tsx @@ -2,9 +2,10 @@ import { Typography } from 'antd'; import { ExclamationCircleOutlined } from '@ant-design/icons'; +import { ReactNode } from 'react'; interface ErrorMessageProps { - message: string; + message: ReactNode; } const ErrorMessage = ({ message }: ErrorMessageProps) => { diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index 039b7aa05..5ae69cd6b 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -158,7 +158,7 @@ export const getSharedProcessWithBpmn = async (definitionId: string, versionCrea return bpmn; } - const processWithBPMN = { ...processMetaObj, bpmn: bpmn }; + const processWithBPMN = { ...processMetaObj.value, bpmn: bpmn }; return processWithBPMN; } From f899c4eea5846bc07bf41802772837b4ef93818c Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Dec 2025 02:35:33 +0100 Subject: [PATCH 08/43] remove unused import --- src/management-system-v2/app/shared-viewer/page.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index 734aaf6c5..8e594f950 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -30,7 +30,6 @@ import { } from '@/lib/server-error-handling/user-error'; import { Result } from 'neverthrow'; import { ReactNode } from 'react'; -import { undefined } from 'zod'; interface PageProps { searchParams: { From 5982dc14008c778877ae6bfc038fc1abcd2c82eb Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Dec 2025 18:04:29 +0100 Subject: [PATCH 09/43] fix error returns --- src/management-system-v2/lib/data/db/process.ts | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/management-system-v2/lib/data/db/process.ts b/src/management-system-v2/lib/data/db/process.ts index deff5bb13..d8977aa4b 100644 --- a/src/management-system-v2/lib/data/db/process.ts +++ b/src/management-system-v2/lib/data/db/process.ts @@ -176,8 +176,8 @@ export async function checkIfProcessAlreadyExistsForAUserInASpaceByName( }); return ok(!!existingProcess); - } catch (err: any) { - return err(new Error('Error checking if process exists by name:', err.message)); + } catch (error: any) { + return err(new Error('Error checking if process exists by name:', error.message)); } } @@ -211,8 +211,8 @@ export async function checkIfProcessAlreadyExistsForAUserInASpaceByNameWithBatch // Return an array of booleans per process return ok(processes.map(({ name, folderId }) => existingSet.has(`${name}:::${folderId}`))); - } catch (err: any) { - return err(new Error(`Error checking process names in batch: ${err.message}`)); + } catch (error: any) { + return err(new Error(`Error checking process names in batch: ${error.message}`)); } } @@ -654,7 +654,8 @@ export async function getProcessVersionBpmn(processDefinitionsId: string, versio where: { id: versionId }, }); - return ok(((await retrieveFile(versn?.bpmnFilePath!, false)) as Buffer).toString('utf8')); + const bpmn = ((await retrieveFile(versn?.bpmnFilePath!, false)) as Buffer).toString('utf8'); + return ok(bpmn); } /** Removes information from the meta data that would not be correct after a restart */ From 39e13f18de8486b3e31ff6cc48308a793de9f618 Mon Sep 17 00:00:00 2001 From: Felipe Trost Date: Wed, 10 Dec 2025 18:05:05 +0100 Subject: [PATCH 10/43] fix(shared-viewer): don't stop import on error --- .../app/shared-viewer/page.tsx | 20 +++++-------------- 1 file changed, 5 insertions(+), 15 deletions(-) diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index 8e594f950..723604299 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -118,10 +118,7 @@ const getProcessInfo = async ( * @param bpmn the bpmn of the process to get the imports for * @param knownInfos the object to put the bpmns into */ -const getImportInfos = async ( - bpmn: string, - knownInfos: ImportsInfo, -): Promise<{ error: UserError } | undefined> => { +const getImportInfos = async (bpmn: string, knownInfos: ImportsInfo): Promise => { // information which tasks reference which processes const taskImportMap = await getDefinitionsAndProcessIdForEveryCallActivity(bpmn, true); @@ -130,7 +127,7 @@ const getImportInfos = async ( if (!(knownInfos[definitionId] && knownInfos[definitionId][versionId])) { const processInfo = await getProcessInfo(definitionId, 0, false, true, versionId); - if (isUserErrorResponse(processInfo)) return processInfo; + if (isUserErrorResponse(processInfo)) continue; const { bpmn: importBpmn } = processInfo.processData; @@ -138,7 +135,7 @@ const getImportInfos = async ( knownInfos[definitionId][versionId] = importBpmn as string; // recursively get the imports of the imports - return await getImportInfos(importBpmn as string, knownInfos); + await getImportInfos(importBpmn as string, knownInfos); } } }; @@ -196,8 +193,6 @@ const SharedViewer = async ({ searchParams }: PageProps) => { return ; } - console.log('processData', processData); - ({ isOwner, processData } = processInfo); if (!processData) { @@ -211,17 +206,12 @@ const SharedViewer = async ({ searchParams }: PageProps) => { let availableImports: ImportsInfo = {}; if (!iframeMode) { - let errorMessage: ReactNode; try { - const error = await getImportInfos(processData.bpmn, availableImports); - if (isUserErrorResponse(error)) errorMessage = error.error.message; + await getImportInfos(processData.bpmn, availableImports); } catch (error) { - errorMessage = getErrorMessage(error); console.error('Failed to resolve the information for process imports: ', error); - } - if (errorMessage) { - return ; + return ; } } From ed698cb88276bc88ff81c65ae9bc752671f25629 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:02:08 +0100 Subject: [PATCH 11/43] Removed debug loggin --- src/management-system-v2/lib/db-seed.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/management-system-v2/lib/db-seed.ts b/src/management-system-v2/lib/db-seed.ts index 55ce88150..0b90583bf 100644 --- a/src/management-system-v2/lib/db-seed.ts +++ b/src/management-system-v2/lib/db-seed.ts @@ -204,10 +204,8 @@ async function writeSeedToDb(seed: DBSeed) { continue; } - console.log('1'); const newUser = await addUser({ ...user, isGuest: false, emailVerifiedOn: null }, tx); if (newUser.isErr()) throw newUser.error; - console.log('2'); const hashedPassword = await hashPassword(user.initialPassword); await setUserPassword(user.id, hashedPassword, tx, true); From 2873e2aeb59cce47e9ce21d0e3743fded9bbd4a2 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:02:30 +0100 Subject: [PATCH 12/43] Removed unused file --- src/management-system-v2/lib/result.ts | 50 -------------------------- 1 file changed, 50 deletions(-) delete mode 100644 src/management-system-v2/lib/result.ts diff --git a/src/management-system-v2/lib/result.ts b/src/management-system-v2/lib/result.ts deleted file mode 100644 index cc6b8d5b4..000000000 --- a/src/management-system-v2/lib/result.ts +++ /dev/null @@ -1,50 +0,0 @@ -export type Result = Ok | Err; - -export class Ok { - readonly isOk = true; - readonly isErr = false; - - constructor(readonly value: T) {} - - map(fn: (value: T) => U): Result { - return new Ok(fn(this.value)); - } - - mapErr(_fn: (error: E) => F): Result { - return new Ok(this.value); - } - - andThen(fn: (value: T) => Result): Result { - return fn(this.value); - } - - match(pattern: { ok: (value: T) => U; err: (error: E) => U }): U { - return pattern.ok(this.value); - } -} - -export class Err { - readonly isOk = false; - readonly isErr = true; - - constructor(readonly error: E) {} - - map(_fn: (value: T) => U): Result { - return new Err(this.error); - } - - mapErr(fn: (error: E) => F): Result { - return new Err(fn(this.error)); - } - - andThen(_fn: (value: T) => Result): Result { - return new Err(this.error); - } - - match(pattern: { ok: (value: T) => U; err: (error: E) => U }): U { - return pattern.err(this.error); - } -} - -export const ok = (value: T): Result => new Ok(value); -export const err = (error: E): Result => new Err(error); From 3ec5a322499f0d0c22a8a0b173c06978b8da7b27 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:09:35 +0100 Subject: [PATCH 13/43] Fixed: If the signin page encounters an error when fetching the user the error page is displayed above the dummy process list page instead of replacing it --- src/management-system-v2/app/(auth)/layout.tsx | 9 ++++++++- src/management-system-v2/app/(auth)/signin/page.tsx | 5 +++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/management-system-v2/app/(auth)/layout.tsx b/src/management-system-v2/app/(auth)/layout.tsx index dbeedfec3..fe8cd401c 100644 --- a/src/management-system-v2/app/(auth)/layout.tsx +++ b/src/management-system-v2/app/(auth)/layout.tsx @@ -1,11 +1,18 @@ import Layout from '@/app/(dashboard)/[environmentId]/layout-client'; +import { getCurrentUser } from '@/components/auth'; import Content from '@/components/content'; import Processes from '@/components/processes'; import { SetAbility } from '@/lib/abilityStore'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; import { FC, PropsWithChildren } from 'react'; import { AiOutlineFile, AiOutlineProfile } from 'react-icons/ai'; -const SigninLayout: FC = ({ children }) => { +const SigninLayout: FC = async ({ children }) => { + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + return errorResponse(currentUser); + } + return ( <> {children} diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index 3f9f9ae1e..bf6f0ced3 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -1,6 +1,6 @@ import { getProviders } from '@/lib/auth'; import { getCurrentUser } from '@/components/auth'; -import { redirect } from 'next/navigation'; +import { notFound, redirect } from 'next/navigation'; import SignIn from './signin'; import { generateGuestReferenceToken } from '@/lib/reference-guest-user-token'; import { env } from '@/lib/ms-config/env-vars'; @@ -13,7 +13,8 @@ const dayInMS = 1000 * 60 * 60 * 24; const SignInPage = async ({ searchParams }: { searchParams: { callbackUrl: string } }) => { const currentUser = await getCurrentUser(); if (currentUser.isErr()) { - return errorResponse(currentUser); + // this should be handled in the layout in the parent folder already + return notFound(); } const { session } = currentUser.value; const isGuest = session?.user.isGuest; From 75125026e1820c4e431ac189a28010fcf296131a Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:09:56 +0100 Subject: [PATCH 14/43] Fixed a typo --- .../[environmentId]/(automation)/executions/layout.tsx | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/layout.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/layout.tsx index d8771df9c..c28a1a738 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/layout.tsx @@ -20,15 +20,15 @@ const ExecutionsLayout: React.FC = async ({ params, childr } const { activeEnvironment } = currentSpace.value; - const exeuctionsSettings = await getSpaceSettingsValues( + const executionsSettings = await getSpaceSettingsValues( activeEnvironment.spaceId, 'process-automation.executions', ); - if (exeuctionsSettings.isErr()) { - return errorResponse(exeuctionsSettings); + if (executionsSettings.isErr()) { + return errorResponse(executionsSettings); } - if (exeuctionsSettings.value.active === false) { + if (executionsSettings.value.active === false) { return notFound(); } From 751fe79d0d239484b9b4d9e9a8a727f9feedf64b Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:13:32 +0100 Subject: [PATCH 15/43] Removed unused code and added/updated some comments --- .../[environmentId]/processes/[mode]/[processId]/page.tsx | 2 -- .../[environmentId]/settings/@processAutomation/page.tsx | 1 - src/management-system-v2/app/admin/systemadmins/page.tsx | 3 ++- src/management-system-v2/app/shared-viewer/page.tsx | 2 -- src/management-system-v2/components/auth.tsx | 4 +--- src/management-system-v2/lib/data/file-manager-facade.ts | 6 ++---- 6 files changed, 5 insertions(+), 13 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx index c6b841a63..d5186898b 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/page.tsx @@ -5,11 +5,9 @@ import Modeler from './modeler'; import { toCaslResource } from '@/lib/ability/caslAbility'; import AddUserControls from '@/components/add-user-controls'; import { getProcess, getProcesses } from '@/lib/data/db/process'; -import { getRolesWithMembers } from '@/lib/data/db/iam/roles'; import { getProcessBPMN } from '@/lib/data/processes'; import BPMNTimeline from '@/components/bpmn-timeline'; import { UnauthorizedError } from '@/lib/ability/abilityHelper'; -import { RoleType, UserType } from './use-potentialOwner-store'; import type { Process } from '@/lib/data/process-schema'; import { redirect } from 'next/navigation'; import { spaceURL } from '@/lib/utils'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@processAutomation/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@processAutomation/page.tsx index 2cefb293b..466c4e6f4 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@processAutomation/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/settings/@processAutomation/page.tsx @@ -15,7 +15,6 @@ const Page = async ({ params }: { params: { environmentId: string } }) => { return errorResponse(currentSpace); } const { - ability, activeEnvironment: { spaceId }, } = currentSpace.value; diff --git a/src/management-system-v2/app/admin/systemadmins/page.tsx b/src/management-system-v2/app/admin/systemadmins/page.tsx index 09daf7762..db8148687 100644 --- a/src/management-system-v2/app/admin/systemadmins/page.tsx +++ b/src/management-system-v2/app/admin/systemadmins/page.tsx @@ -121,7 +121,6 @@ export default async function ManageAdminsPage() { if (!adminData.value) redirect('/'); if (adminData.value.role !== 'admin') return ; - type aa = Promise<(AuthenticatedUser & { role: 'admin' })[]>; const getFullSystemAdmins = async () => { const admins = await getSystemAdmins(); if (admins.isErr()) return admins; @@ -134,6 +133,8 @@ export default async function ManageAdminsPage() { return user; } + // TODO: handle that the user might not be found (can that happen?) + return ok({ ...(user.value as AuthenticatedUser), role: admin.role }); }), ), diff --git a/src/management-system-v2/app/shared-viewer/page.tsx b/src/management-system-v2/app/shared-viewer/page.tsx index 723604299..d02811140 100644 --- a/src/management-system-v2/app/shared-viewer/page.tsx +++ b/src/management-system-v2/app/shared-viewer/page.tsx @@ -23,13 +23,11 @@ import { SettingsOption } from './settings-modal'; import { asyncMap } from '@/lib/helpers/javascriptHelpers'; import { env } from '@/lib/ms-config/env-vars'; import { - UserError, getErrorMessage, isUserErrorResponse, userError, } from '@/lib/server-error-handling/user-error'; import { Result } from 'neverthrow'; -import { ReactNode } from 'react'; interface PageProps { searchParams: { diff --git a/src/management-system-v2/components/auth.tsx b/src/management-system-v2/components/auth.tsx index 76f71c72f..3230d6e2f 100644 --- a/src/management-system-v2/components/auth.tsx +++ b/src/management-system-v2/components/auth.tsx @@ -18,7 +18,6 @@ import { getMSConfig } from '@/lib/ms-config/ms-config'; import { packedStaticRules } from '@/lib/authorization/caslRules'; import { err, ok } from 'neverthrow'; import { UserFacingError } from '@/lib/server-error-handling/user-error'; -import { Prettify } from '@/lib/typescript-utils'; export const getCurrentUser = cache(async () => { if (!env.PROCEED_PUBLIC_IAM_ACTIVE) { @@ -80,7 +79,6 @@ export const getSystemAdminRules = cache((isOrganization: boolean) => { } }); -type a = Awaited>; // TODO: To enable PPR move the session redirect into this function, so it will // be called when the session is first accessed and everything above can PPR. For // permissions, each server component should check its permissions anyway, for @@ -97,7 +95,7 @@ export const getCurrentEnvironment = cache( ) => { const currentUser = await getCurrentUser(); if (currentUser.isErr()) { - return err('pepe'); + return err('Could not get the current user'); } const { userId, systemAdmin } = currentUser.value; diff --git a/src/management-system-v2/lib/data/file-manager-facade.ts b/src/management-system-v2/lib/data/file-manager-facade.ts index 31ec4010c..c302c29dd 100644 --- a/src/management-system-v2/lib/data/file-manager-facade.ts +++ b/src/management-system-v2/lib/data/file-manager-facade.ts @@ -7,13 +7,11 @@ import { ArtifactType, generateProcessFilePath, } from '../helpers/fileManagerHelpers'; -import { contentTypeNotAllowed } from './content-upload-error'; -import { copyFile, deleteFile, retrieveFile, saveFile } from './file-manager/file-manager'; +import { deleteFile, retrieveFile, saveFile } from './file-manager/file-manager'; import db from '@/lib/data/db'; import { getProcessHtmlFormJSON } from './db/process'; -import { asyncMap, findKey } from '../helpers/javascriptHelpers'; +import { asyncMap } from '../helpers/javascriptHelpers'; import { Prisma } from '@prisma/client'; -import { use } from 'react'; import { checkValidity } from './processes'; import { env } from '@/lib/ms-config/env-vars'; import { getUsedImagesFromJson } from '@/components/html-form-editor/serialized-format-utils'; From fc39cd0b72de75329adf119b27542e661b98d018 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:14:47 +0100 Subject: [PATCH 16/43] Fixed a small typo --- src/management-system-v2/app/admin/spaces/page.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/app/admin/spaces/page.tsx b/src/management-system-v2/app/admin/spaces/page.tsx index d275eb617..decbca83d 100644 --- a/src/management-system-v2/app/admin/spaces/page.tsx +++ b/src/management-system-v2/app/admin/spaces/page.tsx @@ -34,7 +34,7 @@ async function deleteSpace(spaceIds: string[]) { } export type deleteSpace = typeof deleteSpace; -export default async function SysteAdminDashboard({ params }: { params?: { userId: string } }) { +export default async function SystemAdminDashboard({ params }: { params?: { userId: string } }) { const user = await getCurrentUser(); if (user.isErr()) return errorResponse(user); if (!user.value.session) redirect('/'); From 71105f5fcd6dc1b7772fca51945660ed40bde63d Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:15:49 +0100 Subject: [PATCH 17/43] Fixed a typo --- src/management-system-v2/lib/authorization/authorization.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/management-system-v2/lib/authorization/authorization.ts b/src/management-system-v2/lib/authorization/authorization.ts index b20a4d0fc..05c885b6a 100644 --- a/src/management-system-v2/lib/authorization/authorization.ts +++ b/src/management-system-v2/lib/authorization/authorization.ts @@ -92,9 +92,9 @@ export async function getUserRules(userId: string, environmentId: string) { // TODO: get bough features from db - const getPurhasedFeatures = (_: string) => []; + const getPurchasedFeatures = (_: string) => []; - const purchasedResources = getPurhasedFeatures(environmentId).filter((resource) => + const purchasedResources = getPurchasedFeatures(environmentId).filter((resource) => MSEnabledResources.includes(resource as any), ); From 9de1d034bd846db8713c7f03d7eba5f911c13aa6 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:26:10 +0100 Subject: [PATCH 18/43] Fixed: In some places the code was not correctly updated to reflect that we now have to check if the returned Result object contains a value instead of if the return value is falsy (which in these cases would always be false since a Result object is always returned); added some returns of empty Results in some places to make the functions always return a result instead of a Result or undefined --- .../[environmentId]/profile/page.tsx | 2 +- .../app/transfer-processes/page.tsx | 2 +- .../lib/data/db/process.ts | 23 ++++++++++++------- .../lib/data/db/space-settings.ts | 2 +- .../lib/data/html-forms.ts | 2 +- .../lib/data/processes.tsx | 5 ++-- .../server-actions.ts | 6 ++--- 7 files changed, 24 insertions(+), 18 deletions(-) diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/page.tsx index f4be9a9e5..1e753eea4 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/profile/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/profile/page.tsx @@ -19,7 +19,7 @@ const ProfilePage = async () => { if (userData.isErr()) return errorResponse(userData); const userHasPassword = await getUserPassword(userId); - if (userHasPassword && userHasPassword.isErr()) { + if (userHasPassword.isErr()) { return errorResponse(userHasPassword); } diff --git a/src/management-system-v2/app/transfer-processes/page.tsx b/src/management-system-v2/app/transfer-processes/page.tsx index 44af18ed1..9f53bdd12 100644 --- a/src/management-system-v2/app/transfer-processes/page.tsx +++ b/src/management-system-v2/app/transfer-processes/page.tsx @@ -56,7 +56,7 @@ export default async function TransferProcessesPage({ // accocunt, generating the token above, and before using it, he signed in with a new account. // We only go further then this redirect, if the user signed in with an account that was // already linked to an existing user - if (!possibleGuest || !possibleGuest.value.isGuest) redirect(callbackUrl); + if (!possibleGuest.value?.isGuest) redirect(callbackUrl); // NOTE: this ignores folders const guestProcesses = await getProcesses(guestId); diff --git a/src/management-system-v2/lib/data/db/process.ts b/src/management-system-v2/lib/data/db/process.ts index d8977aa4b..70150ac1a 100644 --- a/src/management-system-v2/lib/data/db/process.ts +++ b/src/management-system-v2/lib/data/db/process.ts @@ -250,7 +250,7 @@ export async function _addProcess( const folderData = await getFolderById(metadata.folderId); if (folderData.isErr()) return folderData; - if (!folderData) return err(new Error('Folder not found')); + if (!folderData.value) return err(new Error('Folder not found')); // TODO check folder permissions here, they're checked in movefolder, // but by then the folder was already created @@ -351,12 +351,12 @@ export async function _updateProcess( processDefinitionsId, newFolderId: metaChanges.folderId, }); - if (moveResult?.isErr()) return moveResult; + if (moveResult.isErr()) return moveResult; //delete metaChanges.folderId; } const newMetaData = await updateProcessMetaData(processDefinitionsId, metaChanges); - if (newMetaData?.isErr()) return newMetaData; + if (newMetaData.isErr()) return newMetaData; if (newBpmn) { try { await db.process.update({ @@ -395,7 +395,7 @@ export async function moveProcess({ if (process.isErr()) { return process; } - if (!process) { + if (!process.value) { return err(new Error('Process not found')); } @@ -503,6 +503,8 @@ export async function _removeProcess(processDefinitionsId: string, _tx?: Prisma. await tx.process.delete({ where: { id: processDefinitionsId } }); eventHandler.dispatch('processRemoved', { processDefinitionsId }); + + return ok(); } catch (error) { console.error(error); return err(error); @@ -528,7 +530,7 @@ export async function addProcessVersion( if (existingProcess.isErr()) { return existingProcess; } - if (!existingProcess) { + if (!existingProcess.value) { return err(new Error('The process for which you try to create a version does not exist')); } @@ -617,7 +619,7 @@ export async function addProcessVersion( } const versionResult = await versionProcessArtifactRefs(processDefinitionsId, version.id); - if (versionResult?.isErr()) { + if (versionResult.isErr()) { return versionResult; } } catch (error) { @@ -1060,8 +1062,7 @@ export async function deleteProcessScriptTask( return processExists; } - // Not sure what should be returned here - if (!processExists.value) return; + if (!processExists.value) return ok(true); try { const res = await checkIfScriptTaskFileExists(processDefinitionsId, taskFileNameWithExtension); @@ -1071,6 +1072,8 @@ export async function deleteProcessScriptTask( if (res.value) { return ok(await deleteProcessArtifact(res.value?.filePath, true)); } + + return ok(true); } catch (error) { logger.debug(`Error removing script task file. Reason:\n${error}`); return err(error); @@ -1096,6 +1099,8 @@ export async function copyProcessArtifactReferences( }, }), ); + + return ok(); } catch (error) { return err(new Error('error copying process artifact references')); } @@ -1117,6 +1122,8 @@ export async function versionProcessArtifactRefs(processId: string, versionId: s }, }), ); + + return ok(); } catch (error) { return err(new Error('error copying process artifact references')); } diff --git a/src/management-system-v2/lib/data/db/space-settings.ts b/src/management-system-v2/lib/data/db/space-settings.ts index f742d4d16..5351e8dfb 100644 --- a/src/management-system-v2/lib/data/db/space-settings.ts +++ b/src/management-system-v2/lib/data/db/space-settings.ts @@ -50,7 +50,7 @@ export async function populateSpaceSettingsGroup( where: { environmentId: spaceId }, }); - if (!settings) return; + if (!settings) return ok(); Object.entries(settings.settings as Record).forEach(([key, value]) => { const path = key.split('.'); diff --git a/src/management-system-v2/lib/data/html-forms.ts b/src/management-system-v2/lib/data/html-forms.ts index 1dbfa68f8..7b3c2deb8 100644 --- a/src/management-system-v2/lib/data/html-forms.ts +++ b/src/management-system-v2/lib/data/html-forms.ts @@ -90,7 +90,7 @@ export const updateHtmlForm = async (formId: string, newData: Partial) export const removeHtmlForms = async (formIds: string[]) => { try { const result = await _removeHtmlForms(formIds); - if (result && result.isErr()) { + if (result.isErr()) { return userError(getErrorMessage(result.error)); } } catch (err) { diff --git a/src/management-system-v2/lib/data/processes.tsx b/src/management-system-v2/lib/data/processes.tsx index 5ae69cd6b..777b38bbf 100644 --- a/src/management-system-v2/lib/data/processes.tsx +++ b/src/management-system-v2/lib/data/processes.tsx @@ -69,7 +69,6 @@ import { saveProcessArtifact } from './file-manager-facade'; import { getRootFolder } from './db/folders'; import { truthyFilter } from '../typescript-utils'; import { Result, ok } from 'neverthrow'; -import { isSupportedCountry } from 'libphonenumber-js'; // FIXME: Check abilities @@ -198,7 +197,7 @@ export const deleteProcesses = async (definitionIds: string[], spaceId: string) if (error) return error; const result = await removeProcess(definitionId); - if (result && result.isErr()) return userError(getErrorMessage(result.error)); + if (result.isErr()) return userError(getErrorMessage(result.error)); } }; @@ -620,7 +619,7 @@ export const createVersion = async ( const bpmn = await _getProcessBpmn(processId); if (bpmn.isErr()) return userError(getErrorMessage(bpmn.error)); - if (!bpmn) return null; + if (!bpmn.value) return null; const bpmnObj = await toBpmnObject(bpmn.value); diff --git a/src/management-system-v2/lib/email-verification-tokens/server-actions.ts b/src/management-system-v2/lib/email-verification-tokens/server-actions.ts index b1f6a7737..09dfab366 100644 --- a/src/management-system-v2/lib/email-verification-tokens/server-actions.ts +++ b/src/management-system-v2/lib/email-verification-tokens/server-actions.ts @@ -73,9 +73,9 @@ export async function changeEmail(token: string, identifier: string, cancel: boo } if ( - !verificationToken || - verificationToken.value?.type !== 'change_email' || - verificationToken.value?.userId !== userId || + !verificationToken.value || + verificationToken.value.type !== 'change_email' || + verificationToken.value.userId !== userId || !(await notExpired(verificationToken.value)) ) return userError('Invalid token'); From 49819b703f433fa4c713e4265fef8267662d15b8 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:28:39 +0100 Subject: [PATCH 19/43] Fixed: the check for getUserById returning a falsy value will always be false since the function always returns a Result --- src/management-system-v2/instrumentation.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/instrumentation.ts b/src/management-system-v2/instrumentation.ts index 7db70a4c0..a8ac6433f 100644 --- a/src/management-system-v2/instrumentation.ts +++ b/src/management-system-v2/instrumentation.ts @@ -8,7 +8,8 @@ export async function register() { // Register default admin user if IAM is not activated const { userId, createUserArgs } = await import('./lib/no-iam-user'); const { addUser, getUserById } = await import('./lib/data/db/iam/users'); - if (!env.PROCEED_PUBLIC_IAM_ACTIVE && !(await getUserById(userId))) + const user = await getUserById(userId); + if (!env.PROCEED_PUBLIC_IAM_ACTIVE && (user.isErr() || !user.value)) await addUser(createUserArgs); // Create personal spaces for users that don't have one From 8ac1bf8cf6f6969dee0ba263790a5a558a805f28 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:29:40 +0100 Subject: [PATCH 20/43] Fixed: the retry button that is shown on some pages when something fails has no text --- .../lib/server-error-handling/retry-button.tsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/server-error-handling/retry-button.tsx b/src/management-system-v2/lib/server-error-handling/retry-button.tsx index 76cb73bba..3ae906cc2 100644 --- a/src/management-system-v2/lib/server-error-handling/retry-button.tsx +++ b/src/management-system-v2/lib/server-error-handling/retry-button.tsx @@ -6,5 +6,9 @@ import { useRouter } from 'next/navigation'; export default function RetryButton(props: ButtonProps) { const router = useRouter(); - return + ); } From bbc8e7288ec5becf4d6b1fe568a990c7b2bb8372 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Tue, 27 Jan 2026 19:46:47 +0100 Subject: [PATCH 21/43] Some code improvements --- .../processes/[processId]/images/route.ts | 6 +++--- .../organizationEnvironmentRolesHelper.ts | 19 +++++++++---------- src/management-system-v2/lib/data/roles.ts | 10 ++-------- 3 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts b/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts index 3cef9237c..9c9768a55 100644 --- a/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts +++ b/src/management-system-v2/app/api/private/[environmentId]/processes/[processId]/images/route.ts @@ -4,7 +4,6 @@ import { getProcess, getProcessImageFileNames, saveProcessImage } from '@/lib/da import { NextRequest, NextResponse } from 'next/server'; import { v4 } from 'uuid'; import { invalidRequest, readImage } from '../../../image-helpers'; -import { errorResponse } from '@/lib/server-error-handling/page-error-response'; import { getErrorMessage } from '@/lib/server-error-handling/user-error'; export async function GET( @@ -16,6 +15,7 @@ export async function GET( try { const currentSpace = await getCurrentEnvironment(environmentId); if (currentSpace.isErr()) throw currentSpace.error; + const { ability } = currentSpace.value; const process = await getProcess(processId, false); if (process.isErr()) throw process.error; @@ -27,7 +27,7 @@ export async function GET( }); } - if (!currentSpace.value.ability.can('view', toCaslResource('Process', process.value))) { + if (!ability.can('view', toCaslResource('Process', process.value))) { return new NextResponse(null, { status: 403, statusText: 'Not allowed to view image filenames in this process', @@ -57,7 +57,7 @@ export async function POST( const currentEnvironment = await getCurrentEnvironment(environmentId); if (currentEnvironment.isErr()) throw currentEnvironment.error; - const ability = currentEnvironment.value.ability; + const { ability } = currentEnvironment.value; const process = await getProcess(processId, false); if (process.isErr()) throw process.error; diff --git a/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts b/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts index b569f585e..cec11005b 100644 --- a/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts +++ b/src/management-system-v2/lib/authorization/organizationEnvironmentRolesHelper.ts @@ -2,12 +2,14 @@ import { Role } from '../data/role-schema'; import { getRoleById, getRoles } from '../data/db/iam/roles'; import { isMember } from '../data/db/iam/memberships'; import { getRoleMappingByUserId } from '../data/db/iam/role-mappings'; -import { ok } from 'neverthrow'; +import { ok, err, Result } from 'neverthrow'; +import { truthyFilter } from '../typescript-utils'; /** Returns all roles that are applied to a user in a given organization environment */ export async function getAppliedRolesForUser(userId: string, environmentId: string) { // enforces environment to be an organization - if (!isMember(environmentId, userId)) throw new Error('User is not a member of this environment'); + if (!isMember(environmentId, userId)) + return err(new Error('User is not a member of this environment')); const environmentRoles = await getRoles(environmentId); if (environmentRoles.isErr()) { @@ -31,16 +33,13 @@ export async function getAppliedRolesForUser(userId: string, environmentId: stri return roleMappings; } - const roleResults = await Promise.all( - roleMappings.value.map((mapping) => getRoleById(mapping.roleId)), + const roleResults = Result.combine( + await Promise.all(roleMappings.value.map((mapping) => getRoleById(mapping.roleId))), ); - for (const role of roleResults) { - if (role && role.isErr()) { - return role; - } - if (role.value) userRoles.push(role.value); - } + if (roleResults.isErr()) return roleResults; + + roleResults.value.filter(truthyFilter).forEach((role) => userRoles.push(role)); return ok(userRoles); } diff --git a/src/management-system-v2/lib/data/roles.ts b/src/management-system-v2/lib/data/roles.ts index aab9894d2..3ba614b54 100644 --- a/src/management-system-v2/lib/data/roles.ts +++ b/src/management-system-v2/lib/data/roles.ts @@ -35,16 +35,10 @@ export async function addRole(environmentId: string, role: Parameters Date: Tue, 27 Jan 2026 19:56:49 +0100 Subject: [PATCH 22/43] Fixed: cannot login as dev user johndoe when the user does not exist in the db already; Fixed: getUserById signature does not show that the function might return null; Fixed: cannot add users with an email address --- src/management-system-v2/lib/auth.ts | 19 ++++++++++-------- .../lib/data/db/iam/users.ts | 20 ++++++++++--------- 2 files changed, 22 insertions(+), 17 deletions(-) diff --git a/src/management-system-v2/lib/auth.ts b/src/management-system-v2/lib/auth.ts index 5568bfccf..c5c59ebdd 100644 --- a/src/management-system-v2/lib/auth.ts +++ b/src/management-system-v2/lib/auth.ts @@ -29,6 +29,7 @@ import db from './data/db'; import { createUserRegistrationToken } from './email-verification-tokens/utils'; import { saveEmailVerificationToken } from './data/db/iam/verification-tokens'; import { NextAuthEmailTakenError, NextAuthUsernameTakenError } from './authjs-error-message'; +import { NotFoundError } from './server-error-handling/errors'; const nextAuthOptions: NextAuthConfig = { secret: env.NEXTAUTH_SECRET, @@ -56,7 +57,7 @@ const nextAuthOptions: NextAuthConfig = { return token; } - let user = _user as User | undefined; + let user = (_user as User | undefined) || null; if (trigger === 'update') { const newUserData = await getUserById(token.user.id); @@ -64,7 +65,7 @@ const nextAuthOptions: NextAuthConfig = { throw newUserData.error; } - user = newUserData.value as User; + user = newUserData.value; } if (user) token.user = user; @@ -96,12 +97,13 @@ const nextAuthOptions: NextAuthConfig = { if (sessionUserInDb.isErr()) { throw sessionUserInDb.error; } - if (!sessionUserInDb || !sessionUserInDb.value.isGuest) - throw new Error('Something went wrong'); + if (!sessionUserInDb.value?.isGuest) throw new Error('Something went wrong'); const userSigningIn = _user.id ? await getUserById(_user.id) : null; - if (!userSigningIn) { + if (userSigningIn && userSigningIn.isErr()) throw new Error('Something went wrong'); + + if (!userSigningIn || !userSigningIn.value) { const user = _user as Partial; await updateUser(sessionUser.id, { firstName: user.firstName ?? undefined, @@ -128,7 +130,7 @@ const nextAuthOptions: NextAuthConfig = { if (user.isErr()) { throw user.error; } - if (user) { + if (user.value) { if (!user.value.isGuest) { console.warn('User with invalid session'); return; @@ -278,8 +280,9 @@ if (env.NODE_ENV === 'development') { let user; if (credentials.username === 'johndoe') { - let user = await getUserByUsername('johndoe'); - if (!user) user = await addUser(johnDoeTemplate); + let u = await getUserByUsername('johndoe'); + if (u.isOk() && !u.value) user = await addUser(johnDoeTemplate); + else user = u; } else if (credentials.username === 'admin') { user = await getUserByUsername('admin'); } diff --git a/src/management-system-v2/lib/data/db/iam/users.ts b/src/management-system-v2/lib/data/db/iam/users.ts index f0ba17ff1..ce791097a 100644 --- a/src/management-system-v2/lib/data/db/iam/users.ts +++ b/src/management-system-v2/lib/data/db/iam/users.ts @@ -18,6 +18,7 @@ import { UserFacingError } from '@/lib/server-error-handling/user-error'; import { env } from '@/lib/ms-config/env-vars'; import { NextAuthEmailTakenError, NextAuthUsernameTakenError } from '@/lib/authjs-error-message'; import { ensureTransactionWrapper } from '../util'; +import { NotFoundError } from '@/lib/server-error-handling/errors'; export async function getUsers(page: number = 1, pageSize: number = 10) { // TODO ability check @@ -52,13 +53,13 @@ export async function getUserById( if (!user && opts && opts.throwIfNotFound) return err(new Error('User not found')); - return ok(user as User); + return ok(user as User | null); } export async function getUserByEmail(email: string) { const user = await db.user.findUnique({ where: { email: email } }); - if (!user) return err(new Error('User not found')); + if (!user) return err(new NotFoundError(`User could not be found (email: ${email}).`)); return ok(user as User); } @@ -66,9 +67,10 @@ export async function getUserByEmail(email: string) { export async function getUserByUsername(username: string, opts?: { throwIfNotFound?: boolean }) { const user = await db.user.findUnique({ where: { username } }); - if (!user && opts?.throwIfNotFound) return err(new Error('User not found')); + if (!user && opts?.throwIfNotFound) + return err(new NotFoundError(`User could not be found (username: ${username}).`)); - return ok(user as User); + return ok(user as User | null); } export const addUser = ensureTransactionWrapper(_addUser, 1); @@ -92,14 +94,14 @@ export async function _addUser( const [usernameRes, emailRes] = await Promise.all(checks); if (usernameRes) { - if (usernameRes.isErr()) return usernameRes; + if (usernameRes.isErr() && !(usernameRes.error instanceof NotFoundError)) return usernameRes; - if (usernameRes.value) return err(new NextAuthUsernameTakenError()); + if (!usernameRes.isErr() && usernameRes.value) return err(new NextAuthUsernameTakenError()); } if (emailRes) { - if (emailRes.isErr()) return emailRes; + if (emailRes.isErr() && !(emailRes.error instanceof NotFoundError)) return emailRes; - if (emailRes.value) return err(new NextAuthEmailTakenError()); + if (!emailRes.isErr() && emailRes.value) return err(new NextAuthEmailTakenError()); } } @@ -239,7 +241,7 @@ export async function updateUser( return err(new UserFacingError('The username is already taken')); } - if (!user.value.isGuest && user.value.username === 'admin' && 'username' in newUserData) { + if (!user.value?.isGuest && user.value?.username === 'admin' && 'username' in newUserData) { return err(new UserFacingError('The username "admin" cannot be changed')); } From 73c38ebff22974758d0944161fe2460ca3ef233b Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 28 Jan 2026 16:37:17 +0100 Subject: [PATCH 23/43] Readded some code that was unused but still required --- .../[definitionId]/images/[fileName]/route.ts | 9 +++++--- .../[instanceId]/file/[fileName]/route.ts | 21 +++++++++++-------- .../app/api/spaces/[spaceId]/route.ts | 4 ++-- 3 files changed, 20 insertions(+), 14 deletions(-) diff --git a/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/images/[fileName]/route.ts b/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/images/[fileName]/route.ts index 277036723..ab03a9640 100644 --- a/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/images/[fileName]/route.ts +++ b/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/images/[fileName]/route.ts @@ -3,9 +3,12 @@ import { auth } from '@/lib/auth'; import { getProcessImage } from '@/lib/engines/server-actions'; import { NextRequest, NextResponse } from 'next/server'; -export async function GET(props: { - params: Promise<{ environmentId: string; definitionId: string; fileName: string }>; -}) { +export async function GET( + request: NextRequest, + props: { + params: Promise<{ environmentId: string; definitionId: string; fileName: string }>; + }, +) { const params = await props.params; const { environmentId, definitionId, fileName } = params; diff --git a/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/instance/[instanceId]/file/[fileName]/route.ts b/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/instance/[instanceId]/file/[fileName]/route.ts index f7551957a..70c653afd 100644 --- a/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/instance/[instanceId]/file/[fileName]/route.ts +++ b/src/management-system-v2/app/api/private/[environmentId]/engine/resources/process/[definitionId]/instance/[instanceId]/file/[fileName]/route.ts @@ -1,16 +1,19 @@ import { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { auth } from '@/lib/auth'; import { getFile } from '@/lib/engines/server-actions'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; -export async function GET(context: { - params: Promise<{ - environmentId: string; - definitionId: string; - instanceId: string; - fileName: string; - }>; -}) { +export async function GET( + request: NextRequest, + context: { + params: Promise<{ + environmentId: string; + definitionId: string; + instanceId: string; + fileName: string; + }>; + }, +) { const { environmentId, definitionId, instanceId, fileName } = await context.params; const session = await auth(); if (!session) throw new UnauthorizedError(); diff --git a/src/management-system-v2/app/api/spaces/[spaceId]/route.ts b/src/management-system-v2/app/api/spaces/[spaceId]/route.ts index e6543fb4e..78e790824 100644 --- a/src/management-system-v2/app/api/spaces/[spaceId]/route.ts +++ b/src/management-system-v2/app/api/spaces/[spaceId]/route.ts @@ -1,8 +1,8 @@ import db from '@/lib/data/db'; import { env } from '@/lib/ms-config/env-vars'; -import { NextResponse } from 'next/server'; +import { NextRequest, NextResponse } from 'next/server'; -export async function GET(props: { params: Promise<{ spaceId: string }> }) { +export async function GET(request: NextRequest, props: { params: Promise<{ spaceId: string }> }) { const params = await props.params; const { spaceId } = params; From 948c4666a21a11463f31ebd076658d8ccc5a75cc Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 28 Jan 2026 17:40:39 +0100 Subject: [PATCH 24/43] Fixed: Register as New User is not working, logging in after registering as a new user is not working --- src/management-system-v2/lib/auth.ts | 8 +++++--- src/management-system-v2/lib/data/db/iam/users.ts | 2 +- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/management-system-v2/lib/auth.ts b/src/management-system-v2/lib/auth.ts index 8e23f01dd..7cd4caaba 100644 --- a/src/management-system-v2/lib/auth.ts +++ b/src/management-system-v2/lib/auth.ts @@ -29,6 +29,7 @@ import db from './data/db'; import { createUserRegistrationToken } from './email-verification-tokens/utils'; import { saveEmailVerificationToken } from './data/db/iam/verification-tokens'; import { NextAuthEmailTakenError, NextAuthUsernameTakenError } from './authjs-error-message'; +import { NotFoundError } from './server-error-handling/errors'; const nextAuthOptions: NextAuthConfig = { secret: env.NEXTAUTH_SECRET, @@ -404,13 +405,14 @@ if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE || env.PROCEED_PUBLIC_IAM_ // Whenever the email is active, we create the user after he verifies his email if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { const [existingUserUsername, existingUserMail] = await Promise.all([ - getUserByUsername(credentials.username as string), + getUserByUsername(credentials.username as string, { throwIfNotFound: true }), getUserByEmail(credentials.email as string), ]); - if (existingUserUsername) { + + if (existingUserUsername.isOk()) { throw new NextAuthUsernameTakenError(); } - if (existingUserMail) { + if (existingUserMail.isOk()) { throw new NextAuthEmailTakenError(); } diff --git a/src/management-system-v2/lib/data/db/iam/users.ts b/src/management-system-v2/lib/data/db/iam/users.ts index ce791097a..1dace9e92 100644 --- a/src/management-system-v2/lib/data/db/iam/users.ts +++ b/src/management-system-v2/lib/data/db/iam/users.ts @@ -259,7 +259,7 @@ export async function updateUser( return err(new UserFacingError('The username is already taken')); } - updatedUser = { ...user, ...newUserData }; + updatedUser = { ...user.value, ...newUserData }; } const updatedUserFromDB = await dbMutator.user.update({ From d988ec710eb2650bb83f2df748fd6263ca4efa8e Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Wed, 28 Jan 2026 18:58:06 +0100 Subject: [PATCH 25/43] Fixed: Sign In With E-Mail is not working; The link that is generated to sign in when Registering as new User is not working --- .../lib/auth-database-adapter.ts | 34 ++++++++++++++----- src/management-system-v2/lib/auth.ts | 9 +++-- .../lib/data/db/iam/users.ts | 15 ++++---- 3 files changed, 39 insertions(+), 19 deletions(-) diff --git a/src/management-system-v2/lib/auth-database-adapter.ts b/src/management-system-v2/lib/auth-database-adapter.ts index fb5e4e4fe..ab8dca9b6 100644 --- a/src/management-system-v2/lib/auth-database-adapter.ts +++ b/src/management-system-v2/lib/auth-database-adapter.ts @@ -17,28 +17,40 @@ const Adapter = { createUser: async ( user: Omit | { email: string; emailVerified: Date }, ) => { - return addUser({ + const newUser = await addUser({ ...user, profileImage: 'image' in user && typeof user.image === 'string' ? user.image : null, isGuest: false, emailVerifiedOn: null, }); + + if (newUser.isErr()) throw newUser.error; + + return newUser.value; }, getUser: async (id: string) => { - return getUserById(id); + const user = await getUserById(id); + if (user.isErr()) throw user.error; + return user.value; }, updateUser: async (user: AuthenticatedUser) => { - return updateUser(user.id, { ...user, isGuest: false }); + const res = await updateUser(user.id, { ...user, isGuest: false }); + if (res.isErr()) throw res.error; + return res.value; }, getUserByEmail: async (email: string) => { - return getUserByEmail(email) ?? null; + const user = await getUserByEmail(email); + if (user.isErr()) throw user.error; + return user.value; }, createVerificationToken: async ({ expires, ...token }: VerificationToken) => { - return await saveEmailVerificationToken({ + const newToken = await saveEmailVerificationToken({ type: 'signin_with_email', ...token, expires, }); + if (newToken.isErr()) throw newToken.error; + return newToken.value; }, useVerificationToken: async (params: { identifier: string; token: string }) => { try { @@ -49,19 +61,21 @@ const Adapter = { } if (token.value.type === 'signin_with_email' || token.value.type === 'register_new_user') - return token; + return token.value; else return null; } catch (_) { return null; } }, linkAccount: async (account: AdapterAccount) => { - return addOauthAccount({ + const newAccount = await addOauthAccount({ userId: account.userId, type: account.type, provider: account.provider, providerAccountId: account.providerAccountId, }); + if (newAccount.isErr()) throw newAccount.error; + return newAccount.value; }, getUserByAccount: async (account: AdapterAccount) => { const userAccount = await getOauthAccountByProviderId( @@ -69,12 +83,14 @@ const Adapter = { account.providerAccountId, ); if (userAccount.isErr()) { - return userAccount; + throw userAccount.error; } if (!userAccount.value) return null; - return getUserById(userAccount.value.userId) as unknown as AdapterAccount; + const user = await getUserById(userAccount.value.userId); + if (user.isErr()) throw user.error; + return user.value as unknown as AdapterAccount; }, }; diff --git a/src/management-system-v2/lib/auth.ts b/src/management-system-v2/lib/auth.ts index 7cd4caaba..69c1df317 100644 --- a/src/management-system-v2/lib/auth.ts +++ b/src/management-system-v2/lib/auth.ts @@ -405,14 +405,17 @@ if (env.PROCEED_PUBLIC_IAM_LOGIN_USER_PASSWORD_ACTIVE || env.PROCEED_PUBLIC_IAM_ // Whenever the email is active, we create the user after he verifies his email if (env.PROCEED_PUBLIC_IAM_LOGIN_MAIL_ACTIVE) { const [existingUserUsername, existingUserMail] = await Promise.all([ - getUserByUsername(credentials.username as string, { throwIfNotFound: true }), + getUserByUsername(credentials.username as string), getUserByEmail(credentials.email as string), ]); - if (existingUserUsername.isOk()) { + if (existingUserUsername.isErr()) throw existingUserUsername.error; + if (existingUserMail.isErr()) throw existingUserMail.error; + + if (existingUserUsername.value) { throw new NextAuthUsernameTakenError(); } - if (existingUserMail.isOk()) { + if (existingUserMail.value) { throw new NextAuthEmailTakenError(); } diff --git a/src/management-system-v2/lib/data/db/iam/users.ts b/src/management-system-v2/lib/data/db/iam/users.ts index 1dace9e92..022391e9c 100644 --- a/src/management-system-v2/lib/data/db/iam/users.ts +++ b/src/management-system-v2/lib/data/db/iam/users.ts @@ -56,12 +56,13 @@ export async function getUserById( return ok(user as User | null); } -export async function getUserByEmail(email: string) { +export async function getUserByEmail(email: string, opts?: { throwIfNotFound?: boolean }) { const user = await db.user.findUnique({ where: { email: email } }); - if (!user) return err(new NotFoundError(`User could not be found (email: ${email}).`)); + if (!user && opts?.throwIfNotFound) + return err(new NotFoundError(`User could not be found (email: ${email}).`)); - return ok(user as User); + return ok(user as User | null); } export async function getUserByUsername(username: string, opts?: { throwIfNotFound?: boolean }) { @@ -94,14 +95,14 @@ export async function _addUser( const [usernameRes, emailRes] = await Promise.all(checks); if (usernameRes) { - if (usernameRes.isErr() && !(usernameRes.error instanceof NotFoundError)) return usernameRes; + if (usernameRes.isErr()) return usernameRes; - if (!usernameRes.isErr() && usernameRes.value) return err(new NextAuthUsernameTakenError()); + if (usernameRes.value) return err(new NextAuthUsernameTakenError()); } if (emailRes) { - if (emailRes.isErr() && !(emailRes.error instanceof NotFoundError)) return emailRes; + if (emailRes.isErr()) return emailRes; - if (!emailRes.isErr() && emailRes.value) return err(new NextAuthEmailTakenError()); + if (emailRes.value) return err(new NextAuthEmailTakenError()); } } From 9133d42c57f9d9537b56f78db8c3942390a42ca5 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Thu, 29 Jan 2026 11:37:31 +0100 Subject: [PATCH 26/43] Fixed: if isMember is called without a transaction client the function will throw an error --- src/management-system-v2/lib/data/db/iam/memberships.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/management-system-v2/lib/data/db/iam/memberships.ts b/src/management-system-v2/lib/data/db/iam/memberships.ts index af358b94a..53400e674 100644 --- a/src/management-system-v2/lib/data/db/iam/memberships.ts +++ b/src/management-system-v2/lib/data/db/iam/memberships.ts @@ -126,7 +126,7 @@ export async function isMember( userId: string, tx?: Prisma.TransactionClient, ) { - const dbMutator = tx!; + const dbMutator = tx ? tx : db; const environment = await getEnvironmentById(environmentId, undefined, undefined, tx); if (environment.isErr()) { From a62187a72e7d53bc97e5b3f845b537215c8f02df Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Fri, 30 Jan 2026 14:24:51 +0100 Subject: [PATCH 27/43] Fixed a typo --- src/management-system-v2/app/(auth)/signin/signin.tsx | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/management-system-v2/app/(auth)/signin/signin.tsx b/src/management-system-v2/app/(auth)/signin/signin.tsx index 23fce962d..2c67cc1fd 100644 --- a/src/management-system-v2/app/(auth)/signin/signin.tsx +++ b/src/management-system-v2/app/(auth)/signin/signin.tsx @@ -147,7 +147,7 @@ const SignIn: FC<{ @@ -163,8 +163,9 @@ const SignIn: FC<{ color: '#434343', }} > - By using the this Platform, you agree to the Terms of Service{' '} - and the storage of functionally essential cookies on your device. + By using the PROCEED Platform, you agree to the{' '} + Terms of Service and the storage of functionally essential + cookies on your device. From 93a49d854f746ba1ba8c1d02e42cc0383386a4e9 Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Fri, 30 Jan 2026 14:35:45 +0100 Subject: [PATCH 28/43] Small comment improvement --- src/management-system-v2/app/(auth)/signin/page.tsx | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/management-system-v2/app/(auth)/signin/page.tsx b/src/management-system-v2/app/(auth)/signin/page.tsx index 34eda7661..43901c6a2 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -14,7 +14,8 @@ const SignInPage = async (props: { searchParams: Promise<{ callbackUrl: string } const searchParams = await props.searchParams; const currentUser = await getCurrentUser(); if (currentUser.isErr()) { - // this should be handled in the layout in the parent folder already + // this shouldn't really occur since it is handled in the layout file in the parent folder + // already return notFound(); } const { session } = currentUser.value; From cd2087241016d317df8a7daefa906594ea09b12c Mon Sep 17 00:00:00 2001 From: Janis Joderi Shoferi Date: Fri, 30 Jan 2026 14:38:16 +0100 Subject: [PATCH 29/43] Fixed: the MS cannot be built due to an unused typescript error directive leading to a typescript error --- src/management-system-v2/components/share-modal/embed-in-web.tsx | 1 - 1 file changed, 1 deletion(-) diff --git a/src/management-system-v2/components/share-modal/embed-in-web.tsx b/src/management-system-v2/components/share-modal/embed-in-web.tsx index d03521dc7..cf1447032 100644 --- a/src/management-system-v2/components/share-modal/embed-in-web.tsx +++ b/src/management-system-v2/components/share-modal/embed-in-web.tsx @@ -135,7 +135,6 @@ const ModelerShareModalOptionEmdedInWeb = ({