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 4846c82fb..43901c6a2 100644 --- a/src/management-system-v2/app/(auth)/signin/page.tsx +++ b/src/management-system-v2/app/(auth)/signin/page.tsx @@ -1,17 +1,24 @@ 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'; import db from '@/lib/data/db'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const dayInMS = 1000 * 60 * 60 * 24; // take in search query const SignInPage = async (props: { searchParams: Promise<{ callbackUrl: string }> }) => { const searchParams = await props.searchParams; - const { session } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + if (currentUser.isErr()) { + // this shouldn't really occur since it is handled in the layout file in the parent folder + // already + return notFound(); + } + const { session } = currentUser.value; const isGuest = session?.user.isGuest; if (session?.user && !isGuest) { 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. diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/[dbEngineId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/[dbEngineId]/page.tsx index e4bf761fe..481bac505 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/[dbEngineId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/[dbEngineId]/page.tsx @@ -9,6 +9,7 @@ import { getDbEngineById } from '@/lib/data/db/engines'; import { getMSConfig } from '@/lib/ms-config/ms-config'; import EngineDashboard from '@/components/engine-dashboard/server-component'; import SpaceLink from '@/components/space-link'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; export type TableEngine = Engine & { id: string }; @@ -24,8 +25,15 @@ export default async function EnginesPage(props: { const dbEngineId = decodeURIComponent(params.dbEngineId); const engineId = decodeURIComponent(searchParams.engineId || ''); - 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 dbEngine = await getDbEngineById(dbEngineId, activeEnvironment.spaceId, ability); + if (dbEngine.isErr()) { + return errorResponse(dbEngine); + } return ( diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/page.tsx index 4ce4afa44..38321e9d3 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/engines/page.tsx @@ -10,6 +10,7 @@ import { getSpaceSettingsValues } from '@/lib/data/db/space-settings'; import { savedEnginesToEngines } from '@/lib/engines/saved-engines-helpers'; import { Engine as DBEngine } from '@prisma/client'; import { spaceURL } from '@/lib/utils'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const getEngineStatus = async (engine: DBEngine) => { const engines = await savedEnginesToEngines([engine]); @@ -26,20 +27,30 @@ const EnginesPage = async (props: { params: Promise<{ environmentId: string }> } if (!msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) return notFound(); const params = await props.params; - const { activeEnvironment, ability } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { activeEnvironment, ability } = currentSpace.value; const machinesSettings = await getSpaceSettingsValues( activeEnvironment.spaceId, 'process-automation.process-engines', ); + if (machinesSettings.isErr()) { + return errorResponse(machinesSettings); + } - if (machinesSettings.active === false) { + if (machinesSettings.value.active === false) { return notFound(); } const engines = await getDbEngines(activeEnvironment.spaceId, ability); + if (engines.isErr()) { + return errorResponse(engines); + } - const enginesWithStatus = engines.map((engine) => { + const enginesWithStatus = engines.value.map((engine) => { return { ...engine, status: ( diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/page.tsx index 7947356fc..9157b87f5 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions-dashboard/page.tsx @@ -4,21 +4,29 @@ import { getCurrentEnvironment } from '@/components/auth'; import { getMSConfig } from '@/lib/ms-config/ms-config'; import { getSpaceSettingsValues } from '@/lib/data/db/space-settings'; import DashboardView from './dashboard-view'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const Page = async (props: any) => { const params = await props.params; const msConfig = await getMSConfig(); if (!msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) return notFound(); - const { activeEnvironment, ability } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { activeEnvironment, ability } = currentSpace.value; const machinesSettings = await getSpaceSettingsValues( activeEnvironment.spaceId, 'process-automation.dashboard', ability, ); + if (machinesSettings.isErr()) { + return errorResponse(machinesSettings); + } - if (machinesSettings.active === false) { + if (machinesSettings.value.active === false) { return notFound(); } diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx index 1276f2f06..6be269b37 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/executions/[processId]/page.tsx @@ -4,9 +4,13 @@ import { getDeployment } from '@/lib/engines/server-actions'; import ProcessDeploymentView from './process-deployment-view'; import { Suspense } from 'react'; import { getCurrentEnvironment } from '@/components/auth'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; +import { err } from 'neverthrow'; async function Deployment({ processId, spaceId }: { processId: string; spaceId: string }) { const deployment = await getDeployment(spaceId, processId); + if (isUserErrorResponse(deployment)) return errorResponse(err()); if (!deployment) { return ( @@ -24,7 +28,11 @@ export default async function Page(props: { }) { const params = await props.params; //TODO: authentication + authorization - const { activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { activeEnvironment } = currentSpace.value; return ( ; @@ -17,14 +18,21 @@ const ExecutionsLayout: React.FC = async (props) => { if (!msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_ACTIVE) return notFound(); - const { activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { activeEnvironment } = currentSpace.value; - const exeuctionsSettings = await getSpaceSettingsValues( + const executionsSettings = await getSpaceSettingsValues( activeEnvironment.spaceId, 'process-automation.executions', ); + if (executionsSettings.isErr()) { + return errorResponse(executionsSettings); + } - if (exeuctionsSettings.active === false) { + if (executionsSettings.value.active === false) { return notFound(); } 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 5fbf356e6..40b8060d8 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,10 +5,10 @@ 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 { Skeleton } from 'antd'; -import { Suspense } from 'react'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import { getDbEngines } from '@/lib/data/db/engines'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { Result, err, ok } from 'neverthrow'; function getDeploymentNames( deployments: T[], @@ -29,30 +29,56 @@ function getDeploymentNames { - const rootFolder = await getRootFolder(activeEnvironment.spaceId, ability); - const folder = await getFolderById(rootFolder.id); - const folderContents = await getFolderContents(folder.id, ability); - return [folder, folderContents]; - })(), - (async () => { - const engines = await getDbEngines(null, ability, 'dont-check'); - return await getDeployedProcessesFromSavedEngines(engines); - })(), - (async () => { - const spaceEngines = await getDbEngines(activeEnvironment.spaceId, ability); - if (isUserErrorResponse(spaceEngines)) return []; - return await getDeployedProcessesFromSavedEngines(spaceEngines); - })(), - ]); + let promises = await Promise.all([ + (async () => { + const favorites = await getUsersFavourites(); + if (isUserErrorResponse(favorites)) return err(favorites); + + return ok(favorites); + })(), + (async () => { + const rootFolder = await getRootFolder(activeEnvironment.spaceId, ability); + if (rootFolder.isErr()) return rootFolder; + + const folder = await getFolderById(rootFolder.value.id); + if (folder.isErr()) return folder; + + const folderContents = await getFolderContents(folder.value.id, ability); + if (folderContents.isErr()) { + return folderContents; + } + + return ok([folder.value, folderContents.value] as const); + })(), + (async () => { + const engines = await getDbEngines(null, ability, 'dont-check'); + if (engines.isErr()) return engines; + + return ok(await getDeployedProcessesFromSavedEngines(engines.value)); + })(), + (async () => { + const spaceEngines = await getDbEngines(activeEnvironment.spaceId, ability); + if (spaceEngines.isErr()) return spaceEngines; + + return ok(await getDeployedProcessesFromSavedEngines(spaceEngines.value)); + })(), + ]); + + const results = Result.combine(promises); + if (results.isErr()) { + return errorResponse(results); + } + + let [favs, [folder, folderContents], deployedInProceed, deployedInSpaceEngines] = results.value; folderContents = folderContents.filter((p) => p.type === 'folder' || p.versions.length); @@ -66,12 +92,14 @@ async function Executions({ environmentId }: { environmentId: string }) { const deployedProcesses = getDeploymentNames(deployedWithRemappedIds); return ( - + + + ); } @@ -79,9 +107,5 @@ export default async function ExecutionsPage(props: { params: Promise<{ environmentId: string }>; }) { const params = await props.params; - return ( - - - - ); + return ; } diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/layout.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/layout.tsx index b1708b9ef..8eb7fa046 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/layout.tsx @@ -3,6 +3,7 @@ import { getSpaceSettingsValues } from '@/lib/data/db/space-settings'; import { notFound } from 'next/navigation'; import { getCurrentEnvironment } from '@/components/auth'; import { getMSConfig } from '@/lib/ms-config/ms-config'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; type AutomationLayoutProps = { params: Promise<{ environmentId: string }>; @@ -18,14 +19,21 @@ const AutomationsLayout: React.FC = async (props) => { return notFound(); } - const { activeEnvironment } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { activeEnvironment } = currentSpace.value; const automationSettings = await getSpaceSettingsValues( activeEnvironment.spaceId, 'process-automation', ); + if (automationSettings.isErr()) { + return errorResponse(automationSettings); + } - if (automationSettings.active === false) { + if (automationSettings.value.active === false) { return notFound(); } diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/projects/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/projects/page.tsx index 2494a9261..f14aee242 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/projects/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/(automation)/projects/page.tsx @@ -4,10 +4,15 @@ import Content from '@/components/content'; import { Space } from 'antd'; import { getCurrentEnvironment } from '@/components/auth'; import { redirect } from 'next/navigation'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const Projects = async (props: { params: Promise<{ environmentId: string }> }) => { const params = await props.params; - const { ability } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability } = currentSpace.value; if (!ability.can('view', 'Setting')) return redirect('/'); return ( diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/general-settings/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/general-settings/page.tsx index 85ca445e8..5c219dfed 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/general-settings/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/general-settings/page.tsx @@ -5,10 +5,15 @@ import SettingsForm from './settings-form'; // Card throws a react children error if you don't import Title separately. import Title from 'antd/es/typography/Title'; import { redirect } from 'next/navigation'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; const GeneralSettingsPage = async (props: { params: Promise<{ environmentId: string }> }) => { const params = await props.params; - const { ability } = await getCurrentEnvironment(params.environmentId); + const currentSpace = await getCurrentEnvironment(params.environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability } = currentSpace.value; if (!ability.can('view', 'Setting')) return redirect('/'); const updateSettings = async (newSettings: Object) => { diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx index f08cf8028..8731ff9e7 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/[roleId]/page.tsx @@ -4,7 +4,7 @@ import { getRoleWithMembersById } from '@/lib/data/db/iam/roles'; import UnauthorizedFallback from '@/components/unauthorized-fallback'; import { getMembers } from '@/lib/data/db/iam/memberships'; import { getUserById } from '@/lib/data/db/iam/users'; -import { Button, Card, Result, Space, Tabs } from 'antd'; +import { Button, Card, Result as AntdResult, Space, Tabs } from 'antd'; import { LeftOutlined } from '@ant-design/icons'; import RoleGeneralData from './roleGeneralData'; import RolePermissions from './rolePermissions'; @@ -12,21 +12,31 @@ import RoleMembers from './role-members'; import { AuthenticatedUser } from '@/lib/data/user-schema'; import SpaceLink from '@/components/space-link'; import { getFolderById } from '@/lib/data/db/folders'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { Result } from 'neverthrow'; const Page = async (props: { params: Promise<{ roleId: string; environmentId: string }> }) => { const params = await props.params; const { roleId, environmentId } = params; - const { ability, activeEnvironment } = await getCurrentEnvironment(environmentId); + const currentSpace = await getCurrentEnvironment(environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; const role = await getRoleWithMembersById(roleId, ability); + if (role.isErr()) { + return errorResponse(role); + } + // if (role && !ability.can('manage', toCaslResource('Role', role))) return ; if (!ability.can('admin', 'All')) return ; - if (!role) + if (!role.value) return ( - ); - const usersInRole = role.members; + const usersInRole = role.value.members; const roleUserSet = new Set(usersInRole.map((member) => member.id)); const memberships = await getMembers(activeEnvironment.spaceId, ability); - const usersNotInRole = (await Promise.all( - memberships - .filter(({ userId }) => !roleUserSet.has(userId)) - .map((user) => getUserById(user.userId)), - )) as AuthenticatedUser[]; + if (memberships.isErr()) return errorResponse(memberships); + + const usersNotInRole = Result.combine( + await Promise.all( + memberships.value + .filter(({ userId }) => !roleUserSet.has(userId)) + .map((user) => getUserById(user.userId)), + ), + ); + if (usersNotInRole.isErr()) { + return errorResponse(usersNotInRole); + } - const roleParentFolder = role.parentId ? await getFolderById(role.parentId, ability) : undefined; + const roleParentFolder = role.value.parentId + ? await getFolderById(role.value.parentId, ability) + : undefined; + if (roleParentFolder && roleParentFolder.isErr()) { + return errorResponse(roleParentFolder); + } const tabs = [ { key: 'generalData', label: 'General Data', - children: , + children: , }, { key: 'permissions', label: 'Permissions', - children: , + children: , }, ]; - if (role.name !== '@everyone' && role.name !== '@guest') { + if (role.value.name !== '@everyone' && role.value.name !== '@guest') { tabs.push({ key: 'members', label: 'Manage Members', children: ( - + ), }); } @@ -84,7 +110,7 @@ const Page = async (props: { params: Promise<{ roleId: string; environmentId: st Roles - {role?.name} + {role.value.name} } > 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/roles/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/roles/page.tsx index bb3fc12eb..06b9e5709 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,19 +3,27 @@ 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 (props: { params: Promise<{ environmentId: string }> }) => { const params = await props.params; - 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/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]/iam/users/page.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/iam/users/page.tsx index d2fb19f8c..52461b04b 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 @@ -3,17 +3,23 @@ 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 { errorResponse } from '@/lib/server-error-handling/page-error-response'; const Page = async (props: { params: Promise<{ environmentId: string }> }) => { const params = await props.params; - 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); + 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 25af777eb..657e013e4 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,6 +43,7 @@ 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 ( props: PropsWithChildren<{ params: Promise<{ environmentId: string }> }>, @@ -50,33 +52,66 @@ const DashboardLayout = async ( const { children } = props; - 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'); @@ -115,7 +150,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 = []; @@ -139,17 +178,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: , @@ -173,14 +215,20 @@ 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 ( msConfig.PROCEED_PUBLIC_PROCESS_AUTOMATION_TASK_EDITOR_ACTIVE && - automationSettings.task_editor?.active !== false + automationSettings.value.task_editor?.active !== false ) { childRegex = '/tasks($|/)'; children.push({ @@ -206,11 +254,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({ @@ -220,7 +268,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({ @@ -230,7 +278,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({ @@ -414,14 +462,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 params = await props.params; - 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')) @@ -17,9 +22,12 @@ const GeneralSettingsPage = async (props: { params: Promise<{ environmentId: str 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]/management/layout.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/management/layout.tsx index 497095652..d5129ecb9 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/management/layout.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/management/layout.tsx @@ -1,13 +1,27 @@ import Content from '@/components/content'; import SettingsPage from '../settings/settings-page'; +import { getCurrentEnvironment } from '@/components/auth'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { UnauthorizedError } from '@/lib/ability/abilityHelper'; +import { err } from 'neverthrow'; -export default function Layout({ +export default async function Layout({ params, ...children }: { params: Promise<{ environmentId: string }>; }) { - // TODO: check if the user has the rights to change the settings + const currentSpace = await getCurrentEnvironment((await params).environmentId); + if (currentSpace.isErr()) { + return errorResponse(currentSpace); + } + const { ability, activeEnvironment } = currentSpace.value; + if ( + !activeEnvironment.isOrganization || + (!ability.can('update', 'Environment') && !ability.can('delete', 'Environment')) + ) { + return errorResponse(err(new UnauthorizedError())); + } return ( 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 7fb330ff3..83ad1295f 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 @@ -26,7 +26,7 @@ import { useEnvironment } from '@/components/auth-can'; import { ShareModal } from '@/components/share-modal/share-modal'; import { useAddControlCallback } from '@/lib/controls-store'; import { spaceURL } from '@/lib/utils'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import useTimelineViewStore from '@/lib/use-timeline-view-store'; import { handleOpenDocumentation } from '../../processes-helper'; import { EnvVarsContext } from '@/components/env-vars-context'; 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 f0c267127..c148f44c9 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,14 +5,15 @@ 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'; +import { errorResponse } from '@/lib/server-error-handling/page-error-response'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; +import { err } from 'neverthrow'; import { getFolderById } from '@/lib/data/db/folders'; type ProcessPageProps = { @@ -35,24 +36,32 @@ const ProcessComponent = async (props: ProcessComponentProps) => { //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 (props.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); } } const processes = await getProcesses(activeEnvironment.spaceId, ability, false); - const folder = await getFolderById(process.folderId, ability); + const folder = await getFolderById(process.value.folderId, ability); + if (folder.isErr()) return errorResponse(folder); // const rawRoles = activeEnvironment.isOrganization // ? await getRolesWithMembers(activeEnvironment.spaceId, ability) @@ -70,15 +79,25 @@ const ProcessComponent = async (props: ProcessComponentProps) => { // 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 @@ -86,8 +105,8 @@ const ProcessComponent = async (props: ProcessComponentProps) => { return ( <> { modelerComponent={ } timelineComponent={ } /> diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/planned-cost-input.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/planned-cost-input.tsx index ce41bf94a..9d03e0a67 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/planned-cost-input.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/planned-cost-input.tsx @@ -1,5 +1,4 @@ import { Input, Select } from 'antd'; -import { EditOutlined } from '@ant-design/icons'; import React, { useEffect, useState } from 'react'; import styles from './planned-cost-input.module.scss'; 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]/script-task-editor/script-task-editor-environment.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor-environment.tsx index a1d1bddf8..abbb95a93 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor-environment.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor-environment.tsx @@ -11,7 +11,7 @@ import { getFolderScriptTasks, getFolderPathScriptTasks } from '@/lib/data/proce import styles from './tab-bar-height.module.scss'; import { FolderTree, TreeNode as FolderTreeNode, generateTreeNode } from '@/components/FolderTree'; import { useQuery } from '@tanstack/react-query'; -import { isUserErrorResponse } from '@/lib/user-error'; +import { isUserErrorResponse } from '@/lib/server-error-handling/user-error'; import type { FolderContentWithScriptTasks } from '@/lib/data/db/process'; import { Folder } from '@/lib/data/folder-schema'; diff --git a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx index 01ac9ee4c..bdc0a17b6 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/script-task-editor/script-task-editor.tsx @@ -1,14 +1,6 @@ 'use client'; -import { - useCallback, - useEffect, - useImperativeHandle, - useMemo, - useRef, - useState, - forwardRef, -} from 'react'; +import { useEffect, useImperativeHandle, useMemo, useRef, useState, forwardRef } from 'react'; import dynamic from 'next/dynamic'; import { Button, @@ -39,7 +31,11 @@ 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 { getErrorMessage, isUserErrorResponse, userError } from '@/lib/user-error'; +import { + getErrorMessage, + 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]/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]/[processId]/user-task-editor.tsx b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/user-task-editor.tsx index 1f53859b1..a40b29373 100644 --- a/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/user-task-editor.tsx +++ b/src/management-system-v2/app/(dashboard)/[environmentId]/processes/[mode]/[processId]/user-task-editor.tsx @@ -318,4 +318,6 @@ const UserTaskEditorModal: React.FC = ({ processId, op ); }; +UserTaskEditor.displayName = 'UserTaskEditor'; + export default UserTaskEditorModal; 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 88e89c7cc..b66d67acd 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,32 +16,37 @@ 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 (props: { params: Promise<{ environmentId: string; folderId?: string; mode: string }>; }) => { const params = await props.params; - - 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,19 +54,32 @@ const ProcessesPage = async (props: { const pathToFolder: ComponentProps['items'] = []; const wrappingFolderIds = [] as string[]; - let currentFolder: Folder | null = folder; + let currentFolder: Folder | null = folder.value; do { pathToFolder.push({ title: ( - {currentFolder.parentId ? currentFolder.name : isListView ? 'List' : 'Editor'} + {currentFolder?.parentId ? currentFolder?.name : isListView ? 'List' : 'Editor'} ), }); 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 +89,11 @@ const ProcessesPage = async (props: { - {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 c780385cb..0474caea3 100644 --- a/src/management-system-v2/app/admin/systemadmins/page.tsx +++ b/src/management-system-v2/app/admin/systemadmins/page.tsx @@ -9,24 +9,34 @@ 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, 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); + const res = await deleteSystemAdmin(adminMapping.value.id); + if (res.isErr()) return userError('Failed to remove admin rights from the user'); } } catch (e) { return userError('Something went wrong'); @@ -36,7 +46,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 +66,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 +112,45 @@ 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 ; + + 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; + } + + // TODO: handle that the user might not be found (can that happen?) + + 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/activateenvironment/route.ts b/src/management-system-v2/app/api/activateenvironment/route.ts index 2f77433bc..238ed8e28 100644 --- a/src/management-system-v2/app/api/activateenvironment/route.ts +++ b/src/management-system-v2/app/api/activateenvironment/route.ts @@ -1,5 +1,5 @@ import { auth } from '@/lib/auth'; -import { activateEnvrionment } from '@/lib/data/db/iam/environments'; +import { activateEnvironment } from '@/lib/data/db/iam/environments'; import { UnauthorizedError } from '@/lib/ability/abilityHelper'; import { redirect } from 'next/navigation'; @@ -17,7 +17,11 @@ export const GET = async (req: Request) => { if (!activationId) return Response.json({ message: 'No activationId provided' }, { status: 400 }); - await activateEnvrionment(activationId, session.user.id); + const res = await activateEnvironment(activationId, session.user.id); + + if (res.isErr()) { + return Response.json({ message: 'Error activating environment' }, { status: 500 }); + } } catch (e) { console.error(e); return Response.json({ message: 'Error activating environment' }, { status: 500 }); 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 14e149006..673bbbca0 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,137 +13,172 @@ 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, props: { params: Promise<{ environmentId: string; processId: string; imageFileName: string }> }, ) { - const params = await props.params; - - const { environmentId, processId, imageFileName } = params; - - 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) { - return new NextResponse(null, { - status: 403, - statusText: 'Not allowed to view image in this process', - }); - } - - const imageBuffer = await getProcessImage(processId, imageFileName); - - const fileType = await fileTypeFromBuffer(imageBuffer); - - if (!fileType) { + try { + const params = await props.params; + + const { environmentId, processId, imageFileName } = params; + + 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: 415, - statusText: 'Can not read file type of requested image', + status: 500, + statusText: getErrorMessage(error), }); } - - const headers = new Headers(); - headers.set('Content-Type', fileType.mime); - - return new NextResponse(imageBuffer, { status: 200, statusText: 'OK', headers }); } export async function PUT( request: NextRequest, props: { params: Promise<{ environmentId: string; processId: string; imageFileName: string }> }, ) { - const params = await props.params; + try { + const params = await props.params; - const { environmentId, processId, imageFileName } = params; + const { environmentId, processId, imageFileName } = params; - const { ability } = await getCurrentEnvironment(environmentId); + 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( request: NextRequest, props: { params: Promise<{ environmentId: string; processId: string; imageFileName: string }> }, ) { - const params = await props.params; + try { + const params = await props.params; - const { environmentId, processId, imageFileName } = params; + const { environmentId, processId, imageFileName } = params; - const { ability } = await getCurrentEnvironment(environmentId); + 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 52bb887b1..005ad6402 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,73 +4,94 @@ import { getProcess, getProcessImageFileNames, saveProcessImage } from '@/lib/da import { NextRequest, NextResponse } from 'next/server'; import { v4 } from 'uuid'; import { invalidRequest, readImage } from '../../../image-helpers'; +import { getErrorMessage } from '@/lib/server-error-handling/user-error'; export async function GET( request: NextRequest, props: { params: Promise<{ environmentId: string; processId: string }> }, ) { - const params = await props.params; + try { + const params = await props.params; - const { environmentId, processId } = params; + const { environmentId, processId } = params; - const { ability } = await getCurrentEnvironment(environmentId); + const currentSpace = await getCurrentEnvironment(environmentId); + if (currentSpace.isErr()) throw currentSpace.error; + const { ability } = currentSpace.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.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( request: NextRequest, props: { params: Promise<{ environmentId: string; processId: string }> }, ) { - const params = await props.params; + try { + const params = await props.params; - const { environmentId, processId } = params; + const { environmentId, processId } = params; - const isInvalidRequest = invalidRequest(request); - if (isInvalidRequest) return isInvalidRequest; + 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; - 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..5eded320a 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 @@ -2,7 +2,7 @@ import { NextRequest, NextResponse } from 'next/server'; import { deleteInactiveGuestUsers } from '@/lib/data/db/iam/users'; // TODO: get this time from the database -const GUESET_INACTIVE_TIME = 1000 * 60 * 60 * 24 * 30; // 30 days +const GUEST_INACTIVE_TIME = 1000 * 60 * 60 * 24 * 30; // 30 days export async function POST(request: NextRequest) { try { @@ -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(GUEST_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/api/spaces/route.ts b/src/management-system-v2/app/api/spaces/route.ts index 7efbe5c7a..3120ff410 100644 --- a/src/management-system-v2/app/api/spaces/route.ts +++ b/src/management-system-v2/app/api/spaces/route.ts @@ -1,8 +1,8 @@ -import { NextRequest, NextResponse } from 'next/server'; +import { NextResponse } from 'next/server'; import db from '@/lib/data/db'; import { env } from '@/lib/ms-config/env-vars'; -export async function GET(request: NextRequest) { +export async function GET() { if (!env.PROCEED_PUBLIC_IAM_ACTIVE) { return NextResponse.json([ { id: 'proceed-default-no-iam-user', type: 'personal', name: 'Default User' }, diff --git a/src/management-system-v2/app/change-email/page.tsx b/src/management-system-v2/app/change-email/page.tsx index f9acf489e..c33da2acb 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() }); @@ -16,7 +17,11 @@ export default async function ChangeEmailPage(props: { searchParams: Promise { 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/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/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 fb8b7a23d..a09ef135f 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,12 @@ 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, + isUserErrorResponse, + userError, +} from '@/lib/server-error-handling/user-error'; +import { Result } from 'neverthrow'; interface PageProps { searchParams: Promise<{ @@ -45,20 +52,27 @@ const getProcessInfo = async ( isImport: boolean, versionId?: string, ) => { - const { session, userId } = await getCurrentUser(); + const currentUser = await getCurrentUser(); + 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 { ability, activeEnvironment } = await getCurrentEnvironment(session?.user.id); + const currentSpace = await getCurrentEnvironment(session?.user.id); + 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)); + // 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,24 +84,26 @@ 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 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.allowIframeTimestamp) || - (!embeddedMode && timestamp !== processData.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'); } } @@ -100,7 +116,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) => { +const getImportInfos = async (bpmn: string, knownInfos: ImportsInfo): Promise => { // information which tasks reference which processes const taskImportMap = await getDefinitionsAndProcessIdForEveryCallActivity(bpmn, true); @@ -109,17 +125,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)) continue; - // 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 + await getImportInfos(importBpmn as string, knownInfos); } } }; @@ -127,20 +141,32 @@ const getImportInfos = async (bpmn: string, knownInfos: ImportsInfo) => { const SharedViewer = async (props: PageProps) => { const searchParams = await props.searchParams; 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; @@ -162,8 +188,8 @@ const SharedViewer = async (props: 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 ; } ({ isOwner, processData } = processInfo); @@ -181,8 +207,10 @@ const SharedViewer = async (props: PageProps) => { if (!iframeMode) { try { await getImportInfos(processData.bpmn, availableImports); - } catch (err) { - console.error('Failed to resolve the information for process imports: ', err); + } catch (error) { + console.error('Failed to resolve the information for process imports: ', error); + + return ; } } @@ -215,7 +243,7 @@ const SharedViewer = async (props: PageProps) => { 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 1725fecb3..b35dc5490 100644 --- a/src/management-system-v2/app/shared-viewer/process-document.tsx +++ b/src/management-system-v2/app/shared-viewer/process-document.tsx @@ -2,8 +2,6 @@ import React, { useEffect, useState } from 'react'; import { useSearchParams } from 'next/navigation'; -import { getProcess } from '@/lib/data/db/process'; - import { Typography, Table, Grid, Image, Spin } from 'antd'; const { Title } = Typography; @@ -18,6 +16,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'; import { generateDateString } from '@/lib/utils'; export type VersionInfo = { @@ -28,7 +27,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..000f3a64f 100644 --- a/src/management-system-v2/app/shared-viewer/workspace-selection.tsx +++ b/src/management-system-v2/app/shared-viewer/workspace-selection.tsx @@ -7,7 +7,6 @@ import { LaptopOutlined, InfoCircleOutlined } from '@ant-design/icons'; import { copyProcesses } from '@/lib/data/processes'; import { Environment } from '@/lib/data/environment-schema'; -import { getProcess } from '@/lib/data/db/process'; import { VersionInfo } from './process-document'; import styles from './workspace-selection.module.scss'; @@ -18,9 +17,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 +66,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 830545543..43589da5b 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(props: { searchParams: Promise<{ @@ -14,7 +15,11 @@ export default async function TransferProcessesPage(props: { }>; }) { const searchParams = await props.searchParams; - 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('/'); @@ -43,17 +48,23 @@ export default async function TransferProcessesPage(props: { 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.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 ( @@ -61,7 +72,8 @@ export default async function TransferProcessesPage(props: { 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 4dd9e148c..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/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/FolderTree.tsx b/src/management-system-v2/components/FolderTree.tsx index b1993267c..4d2d20b60 100644 --- a/src/management-system-v2/components/FolderTree.tsx +++ b/src/management-system-v2/components/FolderTree.tsx @@ -6,7 +6,7 @@ import React, { ReactNode, SetStateAction, useCallback, useEffect, useRef, useSt import { useEnvironment } from './auth-can'; import { ProcessListItemIcon } from './process-list'; import ProceedLoadingIndicator from './loading-proceed'; -import { UserError, isUserErrorResponse } from '@/lib/user-error'; +import { UserError, isUserErrorResponse } from '@/lib/server-error-handling/user-error'; type FolderChildren = { id: string; diff --git a/src/management-system-v2/components/auth-can.tsx b/src/management-system-v2/components/auth-can.tsx index 68a3f8f98..8bf051721 100644 --- a/src/management-system-v2/components/auth-can.tsx +++ b/src/management-system-v2/components/auth-can.tsx @@ -11,7 +11,7 @@ import { toCaslResource, resourceAction, } from '@/lib/ability/caslAbility'; -import { usePathname, useRouter } from 'next/navigation'; +import { useRouter } from 'next/navigation'; import { SpaceContext } from '@/app/(dashboard)/[environmentId]/layout-client'; import { EnvVarsContext } from './env-vars-context'; import { session } from '@/lib/no-iam-user'; diff --git a/src/management-system-v2/components/auth.tsx b/src/management-system-v2/components/auth.tsx index 11b0c843c..3b7d858b4 100644 --- a/src/management-system-v2/components/auth.tsx +++ b/src/management-system-v2/components/auth.tsx @@ -15,17 +15,18 @@ 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 { err, ok } from 'neverthrow'; +import { UserFacingError } from '@/lib/server-error-handling/user-error'; 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 = await 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 err('Could not get the current user'); + } + + const { userId, systemAdmin } = currentUser.value; const msConfig = await getMSConfig(); if ( @@ -107,16 +116,19 @@ 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.'); + return err(new UserFacingError('You are not part of an organization.')); } else { return redirect(`/create-organization`); } } - activeSpace = userOrgs[0]; + activeSpace = userOrgs.value[0]; isOrganization = true; } @@ -125,16 +137,18 @@ 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)) { + const checkIsMember = await isMember(decodeURIComponent(spaceIdParam), userId); + if (checkIsMember.isErr()) return checkIsMember; + if (!userId || !checkIsMember.value) { 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) @@ -146,10 +160,13 @@ export const getCurrentEnvironment = cache( } const ability = await getAbilityForUser(userId, activeSpace); + if (ability.isErr()) { + return ability; + } - return { - ability, + return ok({ + ability: ability.value, 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 4377b4ea5..9b44558e2 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/chatbot.tsx b/src/management-system-v2/components/chatbot.tsx index 4c8a32f2f..ba90410a6 100644 --- a/src/management-system-v2/components/chatbot.tsx +++ b/src/management-system-v2/components/chatbot.tsx @@ -3,12 +3,11 @@ import { FC, useEffect, useMemo, useRef, useState } from 'react'; import styles from './chatbot.module.scss'; import Scrollbar from './scrollbar'; import Message from './chat-message'; -import { Button, Input } from 'antd'; +import { Button } from 'antd'; import { SendOutlined } from '@ant-design/icons'; import TextArea from 'antd/es/input/TextArea'; import { useChatStore } from '@/lib/chat-messages-store'; import cn from 'classnames'; -import { useIntervalLock } from '@/lib/useIntervalLock'; type ChatBotType = {}; diff --git a/src/management-system-v2/components/collapsible-card.tsx b/src/management-system-v2/components/collapsible-card.tsx index cfa66822e..e6736a917 100644 --- a/src/management-system-v2/components/collapsible-card.tsx +++ b/src/management-system-v2/components/collapsible-card.tsx @@ -2,7 +2,7 @@ import { Card, Button } from 'antd'; import { DoubleRightOutlined, DoubleLeftOutlined } from '@ant-design/icons'; -import React, { FC, PropsWithChildren } from 'react'; +import { FC, PropsWithChildren } from 'react'; import classNames from 'classnames'; type CollapsibleCardProps = PropsWithChildren<{ diff --git a/src/management-system-v2/components/confirmation-button.tsx b/src/management-system-v2/components/confirmation-button.tsx index b51ec20d5..360f26242 100644 --- a/src/management-system-v2/components/confirmation-button.tsx +++ b/src/management-system-v2/components/confirmation-button.tsx @@ -1,4 +1,4 @@ -import { ComponentProps, FC, PropsWithChildren, ReactNode, forwardRef, useState } from 'react'; +import { ComponentProps, PropsWithChildren, ReactNode, forwardRef, useState } from 'react'; import { Button, Modal, Tooltip, Typography } from 'antd'; import { useAddControlCallback } from '@/lib/controls-store'; @@ -36,6 +36,22 @@ const ConfirmationButton = forwardRef const [modalOpen, setModalOpen] = useState(false); const [loading, setLoading] = useState(false); + const clearModal = () => { + setModalOpen(false); + onExternalClose?.(); + setLoading(false); + }; + + const onConfirmWrapper = async () => { + setLoading(true); + + try { + await onConfirm(); + } catch (err) {} + + clearModal(); + }; + useAddControlCallback( 'process-list', ['selectall', 'esc', 'copy', 'paste', 'enter', 'cut', 'export', 'import', 'shift+enter'], @@ -57,22 +73,6 @@ const ConfirmationButton = forwardRef { level: 2, blocking: externalOpen || modalOpen }, ); - const clearModal = () => { - setModalOpen(false); - onExternalClose?.(); - setLoading(false); - }; - - const onConfirmWrapper = async () => { - setLoading(true); - - try { - await onConfirm(); - } catch (err) {} - - clearModal(); - }; - return ( <> { diff --git a/src/management-system-v2/components/folder-modal.tsx b/src/management-system-v2/components/folder-modal.tsx index 97f480665..d08dcd18b 100644 --- a/src/management-system-v2/components/folder-modal.tsx +++ b/src/management-system-v2/components/folder-modal.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { useEffect } from 'react'; +import { useEffect } from 'react'; import { Form, Input, Modal } from 'antd'; import type { ModalProps } from 'antd'; import useParseZodErrors, { antDesignInputProps } from '@/lib/useParseZodErrors'; diff --git a/src/management-system-v2/components/folder-move-modal.tsx b/src/management-system-v2/components/folder-move-modal.tsx index 30e47f99c..9b0f0dc18 100644 --- a/src/management-system-v2/components/folder-move-modal.tsx +++ b/src/management-system-v2/components/folder-move-modal.tsx @@ -1,6 +1,6 @@ 'use client'; -import React, { FC } from 'react'; +import { FC } from 'react'; import { FolderTree } from './FolderTree'; import { Button, Modal } from 'antd'; import { FolderChildren } from '@/lib/data/folders'; diff --git a/src/management-system-v2/components/html-form-editor/elements/CheckboxOrRadioGroup.tsx b/src/management-system-v2/components/html-form-editor/elements/CheckboxOrRadioGroup.tsx index 5eea35d95..969392822 100644 --- a/src/management-system-v2/components/html-form-editor/elements/CheckboxOrRadioGroup.tsx +++ b/src/management-system-v2/components/html-form-editor/elements/CheckboxOrRadioGroup.tsx @@ -126,6 +126,7 @@ export const ExportCheckboxOrRadioGroup: React.FC = (
{data.map((entry) => ( = { diff --git a/src/management-system-v2/components/loading-proceed.tsx b/src/management-system-v2/components/loading-proceed.tsx index 2f278283c..005437ea1 100644 --- a/src/management-system-v2/components/loading-proceed.tsx +++ b/src/management-system-v2/components/loading-proceed.tsx @@ -1,4 +1,4 @@ -import React, { FC, useCallback, useRef, PropsWithChildren } from 'react'; +import { FC, useCallback, useRef, PropsWithChildren } from 'react'; import style from './loading-proceed.module.scss'; import { useLazyRendering } from './scrollbar'; import cn from 'classnames'; diff --git a/src/management-system-v2/components/process-icon-list.tsx b/src/management-system-v2/components/process-icon-list.tsx index 8f58452fd..12668dfb2 100644 --- a/src/management-system-v2/components/process-icon-list.tsx +++ b/src/management-system-v2/components/process-icon-list.tsx @@ -1,6 +1,6 @@ 'use client'; -import { Dispatch, FC, SetStateAction, Suspense } from 'react'; +import { Dispatch, FC, SetStateAction } from 'react'; import { InfoCircleOutlined, FolderOutlined } from '@ant-design/icons'; import { LazyBPMNViewer } from './bpmn-viewer'; diff --git a/src/management-system-v2/components/process-import.tsx b/src/management-system-v2/components/process-import.tsx index 49a4b2045..9baf25ce5 100644 --- a/src/management-system-v2/components/process-import.tsx +++ b/src/management-system-v2/components/process-import.tsx @@ -6,9 +6,7 @@ import { Button, Modal, Upload } from 'antd'; import type { ButtonProps } from 'antd'; import { - getDefinitionsId, getDefinitionsInfos, - getDefinitionsName, getElementsByTagName, getProcessDocumentation, toBpmnObject, diff --git a/src/management-system-v2/components/process-info-card.tsx b/src/management-system-v2/components/process-info-card.tsx index e8a7fa942..613aa712e 100644 --- a/src/management-system-v2/components/process-info-card.tsx +++ b/src/management-system-v2/components/process-info-card.tsx @@ -1,13 +1,11 @@ 'use client'; -import React, { useRef, useImperativeHandle, forwardRef } from 'react'; +import { useRef, useImperativeHandle, forwardRef } from 'react'; import CollapsibleCard from './collapsible-card'; import { useUserPreferences } from '@/lib/user-preferences'; import { ProcessListProcess } from './processes/types'; import ResizableElement, { ResizableElementRefType } from './ResizableElement'; import MetaDataContent from './process-info-card-content'; -import dynamic from 'next/dynamic'; -const TextViewer = dynamic(() => import('@/components/text-viewer'), { ssr: false }); type MetaDataType = { selectedElement?: ProcessListProcess; diff --git a/src/management-system-v2/components/process-list.tsx b/src/management-system-v2/components/process-list.tsx index 251b9907e..a8ca2818c 100644 --- a/src/management-system-v2/components/process-list.tsx +++ b/src/management-system-v2/components/process-list.tsx @@ -1,15 +1,6 @@ 'use client'; -import { - Button, - Grid, - Row, - TableColumnType, - TableColumnsType, - TableProps, - Tooltip, - Typography, -} from 'antd'; +import { Button, Grid, Row, TableColumnType, TableColumnsType, TableProps, Tooltip } from 'antd'; import React, { useCallback, FC, diff --git a/src/management-system-v2/components/process-modal.tsx b/src/management-system-v2/components/process-modal.tsx index 4c2996456..5bcfa0bdf 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, 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/components/saved-engines-list.tsx b/src/management-system-v2/components/saved-engines-list.tsx index a75f1788a..0ec23a521 100644 --- a/src/management-system-v2/components/saved-engines-list.tsx +++ b/src/management-system-v2/components/saved-engines-list.tsx @@ -170,7 +170,10 @@ const SavedEnginesList = ({ if (!status.online) return; return status.engines.map((engine) => ( - + {engine.id} )); diff --git a/src/management-system-v2/components/scrollbar.tsx b/src/management-system-v2/components/scrollbar.tsx index fb0a5fe2c..9cfed0072 100644 --- a/src/management-system-v2/components/scrollbar.tsx +++ b/src/management-system-v2/components/scrollbar.tsx @@ -1,5 +1,5 @@ import { useIntervalLock } from '@/lib/useIntervalLock'; -import React, { FC, useEffect, useState, useRef, useCallback, use, MouseEventHandler } from 'react'; +import React, { FC, useEffect, useState, useRef, useCallback, MouseEventHandler } from 'react'; import styles from './scrollbar.module.scss'; import classNames from 'classnames'; 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 = ({