From e77e7e31494930c59c65da3d5d3ad01a6870a3b5 Mon Sep 17 00:00:00 2001 From: Marie <51697796+ijreilly@users.noreply.github.com> Date: Wed, 18 Jun 2025 16:32:29 +0200 Subject: [PATCH] [permissions] Adapt defaultPath to permissions (#12689) We had two linked issues 1. default path was not taking permissions into account and could link to an object user does not have read access on's page 2. visiting the url of an object the user does not have read access on was possible and returned a "blank" page Before https://github.com/user-attachments/assets/e4da1de5-d7e9-4644-ba8e-cd366a9b0fad After https://github.com/user-attachments/assets/6576f662-d3a0-4173-8b48-233cc0a04cdf Also tested with V1. --- .../__tests__/useDefaultHomePagePath.test.ts | 5 +++ .../hooks/useDefaultHomePagePath.ts | 37 +++++++++++++++++-- .../components/RecordIndexContainerGater.tsx | 7 ++-- .../src/testing/mock-data/users.ts | 6 +++ 4 files changed, 48 insertions(+), 7 deletions(-) diff --git a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts index 8b72885a6..04374b388 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/__tests__/useDefaultHomePagePath.test.ts @@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react'; import { RecoilRoot, useSetRecoilState } from 'recoil'; import { currentUserState } from '@/auth/states/currentUserState'; +import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; @@ -23,6 +24,9 @@ const renderHooks = ({ const { result } = renderHook( () => { const setCurrentUser = useSetRecoilState(currentUserState); + const setCurrentUserWorkspace = useSetRecoilState( + currentUserWorkspaceState, + ); const setObjectMetadataItems = useSetRecoilState( objectMetadataItemsState, ); @@ -56,6 +60,7 @@ const renderHooks = ({ if (withCurrentUser) { setCurrentUser(mockedUserData); + setCurrentUserWorkspace(mockedUserData.currentUserWorkspace); } return useDefaultHomePagePath(); }, diff --git a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts index 26b245ce5..a8a965f02 100644 --- a/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts +++ b/packages/twenty-front/src/modules/navigation/hooks/useDefaultHomePagePath.ts @@ -2,8 +2,11 @@ import { currentUserState } from '@/auth/states/currentUserState'; import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState'; import { ObjectPathInfo } from '@/navigation/types/ObjectPathInfo'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState'; import { AppPath } from '@/types/AppPath'; +import { SettingsPath } from '@/types/SettingsPath'; +import isEmpty from 'lodash.isempty'; import { useCallback, useMemo } from 'react'; import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; @@ -11,10 +14,23 @@ import { getAppPath } from '~/utils/navigation/getAppPath'; export const useDefaultHomePagePath = () => { const currentUser = useRecoilValue(currentUserState); + const { objectPermissionsByObjectMetadataId } = useObjectPermissions(); + const { activeNonSystemObjectMetadataItems, alphaSortedActiveNonSystemObjectMetadataItems, } = useFilteredObjectMetadataItems(); + + const readableAlphaSortedActiveNonSystemObjectMetadataItems = useMemo(() => { + return alphaSortedActiveNonSystemObjectMetadataItems.filter((item) => { + const objectPermissions = objectPermissionsByObjectMetadataId[item.id]; + return objectPermissions?.canReadObjectRecords; + }); + }, [ + alphaSortedActiveNonSystemObjectMetadataItems, + objectPermissionsByObjectMetadataId, + ]); + const prefetchViews = useRecoilValue(prefetchViewsState); const lastVisitedObjectMetadataItemId = useRecoilValue( lastVisitedObjectMetadataItemIdState, @@ -39,7 +55,7 @@ export const useDefaultHomePagePath = () => { const firstObjectPathInfo = useMemo(() => { const [firstObjectMetadataItem] = - alphaSortedActiveNonSystemObjectMetadataItems; + readableAlphaSortedActiveNonSystemObjectMetadataItems; if (!isDefined(firstObjectMetadataItem)) { return null; @@ -48,10 +64,14 @@ export const useDefaultHomePagePath = () => { const view = getFirstView(firstObjectMetadataItem?.id); return { objectMetadataItem: firstObjectMetadataItem, view }; - }, [alphaSortedActiveNonSystemObjectMetadataItems, getFirstView]); + }, [readableAlphaSortedActiveNonSystemObjectMetadataItems, getFirstView]); const defaultObjectPathInfo = useMemo(() => { - if (!isDefined(lastVisitedObjectMetadataItemId)) { + if ( + !isDefined(lastVisitedObjectMetadataItemId) || + !objectPermissionsByObjectMetadataId[lastVisitedObjectMetadataItemId] + ?.canReadObjectRecords + ) { return firstObjectPathInfo; } @@ -72,6 +92,7 @@ export const useDefaultHomePagePath = () => { getActiveObjectMetadataItemMatchingId, getFirstView, lastVisitedObjectMetadataItemId, + objectPermissionsByObjectMetadataId, ]); const defaultHomePagePath = useMemo(() => { @@ -79,6 +100,10 @@ export const useDefaultHomePagePath = () => { return AppPath.SignInUp; } + if (isEmpty(readableAlphaSortedActiveNonSystemObjectMetadataItems)) { + return `${AppPath.Settings}/${SettingsPath.ProfilePage}`; + } + if (!isDefined(defaultObjectPathInfo)) { return AppPath.NotFound; } @@ -91,7 +116,11 @@ export const useDefaultHomePagePath = () => { { objectNamePlural: namePlural }, viewId ? { viewId } : undefined, ); - }, [currentUser, defaultObjectPathInfo]); + }, [ + currentUser, + defaultObjectPathInfo, + readableAlphaSortedActiveNonSystemObjectMetadataItems, + ]); return { defaultHomePagePath }; }; diff --git a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx index 16150a2bc..40aef77c9 100644 --- a/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx +++ b/packages/twenty-front/src/modules/object-record/record-index/components/RecordIndexContainerGater.tsx @@ -5,6 +5,8 @@ import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionM import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; +import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; +import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; import { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId'; import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; @@ -23,8 +25,7 @@ import { ViewComponentInstanceContext } from '@/views/states/contexts/ViewCompon import styled from '@emotion/styled'; import { useRecoilCallback } from 'recoil'; import { capitalize } from 'twenty-shared/utils'; -import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; -import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions'; +import { NotFound } from '~/pages/not-found/NotFound'; const StyledIndexContainer = styled.div` display: flex; @@ -68,7 +69,7 @@ export const RecordIndexContainerGater = () => { const hasObjectReadPermissions = objectPermissions.canReadObjectRecords; if (!hasObjectReadPermissions) { - return <>; + return ; } return ( diff --git a/packages/twenty-front/src/testing/mock-data/users.ts b/packages/twenty-front/src/testing/mock-data/users.ts index 3d9ac383d..6f174bcee 100644 --- a/packages/twenty-front/src/testing/mock-data/users.ts +++ b/packages/twenty-front/src/testing/mock-data/users.ts @@ -128,6 +128,12 @@ export const mockedUserData: MockedUser = { currentWorkspace: mockCurrentWorkspace, currentUserWorkspace: { settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS], + objectPermissions: [ + { + objectMetadataId: '4a45f524-b8cb-40e8-8450-28e402b442cf', + canReadObjectRecords: true, + }, + ], }, locale: 'en', workspaces: [{ workspace: mockCurrentWorkspace }],