[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.
This commit is contained in:
Marie
2025-06-18 16:32:29 +02:00
committed by GitHub
parent d284fd1d71
commit e77e7e3149
4 changed files with 48 additions and 7 deletions

View File

@ -2,6 +2,7 @@ import { renderHook } from '@testing-library/react';
import { RecoilRoot, useSetRecoilState } from 'recoil'; import { RecoilRoot, useSetRecoilState } from 'recoil';
import { currentUserState } from '@/auth/states/currentUserState'; import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath'; import { useDefaultHomePagePath } from '@/navigation/hooks/useDefaultHomePagePath';
import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState'; import { objectMetadataItemsState } from '@/object-metadata/states/objectMetadataItemsState';
import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations'; import { AggregateOperations } from '@/object-record/record-table/constants/AggregateOperations';
@ -23,6 +24,9 @@ const renderHooks = ({
const { result } = renderHook( const { result } = renderHook(
() => { () => {
const setCurrentUser = useSetRecoilState(currentUserState); const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentUserWorkspace = useSetRecoilState(
currentUserWorkspaceState,
);
const setObjectMetadataItems = useSetRecoilState( const setObjectMetadataItems = useSetRecoilState(
objectMetadataItemsState, objectMetadataItemsState,
); );
@ -56,6 +60,7 @@ const renderHooks = ({
if (withCurrentUser) { if (withCurrentUser) {
setCurrentUser(mockedUserData); setCurrentUser(mockedUserData);
setCurrentUserWorkspace(mockedUserData.currentUserWorkspace);
} }
return useDefaultHomePagePath(); return useDefaultHomePagePath();
}, },

View File

@ -2,8 +2,11 @@ import { currentUserState } from '@/auth/states/currentUserState';
import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState'; import { lastVisitedObjectMetadataItemIdState } from '@/navigation/states/lastVisitedObjectMetadataItemIdState';
import { ObjectPathInfo } from '@/navigation/types/ObjectPathInfo'; import { ObjectPathInfo } from '@/navigation/types/ObjectPathInfo';
import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems'; import { useFilteredObjectMetadataItems } from '@/object-metadata/hooks/useFilteredObjectMetadataItems';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState'; import { prefetchViewsState } from '@/prefetch/states/prefetchViewsState';
import { AppPath } from '@/types/AppPath'; import { AppPath } from '@/types/AppPath';
import { SettingsPath } from '@/types/SettingsPath';
import isEmpty from 'lodash.isempty';
import { useCallback, useMemo } from 'react'; import { useCallback, useMemo } from 'react';
import { useRecoilValue } from 'recoil'; import { useRecoilValue } from 'recoil';
import { isDefined } from 'twenty-shared/utils'; import { isDefined } from 'twenty-shared/utils';
@ -11,10 +14,23 @@ import { getAppPath } from '~/utils/navigation/getAppPath';
export const useDefaultHomePagePath = () => { export const useDefaultHomePagePath = () => {
const currentUser = useRecoilValue(currentUserState); const currentUser = useRecoilValue(currentUserState);
const { objectPermissionsByObjectMetadataId } = useObjectPermissions();
const { const {
activeNonSystemObjectMetadataItems, activeNonSystemObjectMetadataItems,
alphaSortedActiveNonSystemObjectMetadataItems, alphaSortedActiveNonSystemObjectMetadataItems,
} = useFilteredObjectMetadataItems(); } = useFilteredObjectMetadataItems();
const readableAlphaSortedActiveNonSystemObjectMetadataItems = useMemo(() => {
return alphaSortedActiveNonSystemObjectMetadataItems.filter((item) => {
const objectPermissions = objectPermissionsByObjectMetadataId[item.id];
return objectPermissions?.canReadObjectRecords;
});
}, [
alphaSortedActiveNonSystemObjectMetadataItems,
objectPermissionsByObjectMetadataId,
]);
const prefetchViews = useRecoilValue(prefetchViewsState); const prefetchViews = useRecoilValue(prefetchViewsState);
const lastVisitedObjectMetadataItemId = useRecoilValue( const lastVisitedObjectMetadataItemId = useRecoilValue(
lastVisitedObjectMetadataItemIdState, lastVisitedObjectMetadataItemIdState,
@ -39,7 +55,7 @@ export const useDefaultHomePagePath = () => {
const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => { const firstObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
const [firstObjectMetadataItem] = const [firstObjectMetadataItem] =
alphaSortedActiveNonSystemObjectMetadataItems; readableAlphaSortedActiveNonSystemObjectMetadataItems;
if (!isDefined(firstObjectMetadataItem)) { if (!isDefined(firstObjectMetadataItem)) {
return null; return null;
@ -48,10 +64,14 @@ export const useDefaultHomePagePath = () => {
const view = getFirstView(firstObjectMetadataItem?.id); const view = getFirstView(firstObjectMetadataItem?.id);
return { objectMetadataItem: firstObjectMetadataItem, view }; return { objectMetadataItem: firstObjectMetadataItem, view };
}, [alphaSortedActiveNonSystemObjectMetadataItems, getFirstView]); }, [readableAlphaSortedActiveNonSystemObjectMetadataItems, getFirstView]);
const defaultObjectPathInfo = useMemo<ObjectPathInfo | null>(() => { const defaultObjectPathInfo = useMemo<ObjectPathInfo | null>(() => {
if (!isDefined(lastVisitedObjectMetadataItemId)) { if (
!isDefined(lastVisitedObjectMetadataItemId) ||
!objectPermissionsByObjectMetadataId[lastVisitedObjectMetadataItemId]
?.canReadObjectRecords
) {
return firstObjectPathInfo; return firstObjectPathInfo;
} }
@ -72,6 +92,7 @@ export const useDefaultHomePagePath = () => {
getActiveObjectMetadataItemMatchingId, getActiveObjectMetadataItemMatchingId,
getFirstView, getFirstView,
lastVisitedObjectMetadataItemId, lastVisitedObjectMetadataItemId,
objectPermissionsByObjectMetadataId,
]); ]);
const defaultHomePagePath = useMemo(() => { const defaultHomePagePath = useMemo(() => {
@ -79,6 +100,10 @@ export const useDefaultHomePagePath = () => {
return AppPath.SignInUp; return AppPath.SignInUp;
} }
if (isEmpty(readableAlphaSortedActiveNonSystemObjectMetadataItems)) {
return `${AppPath.Settings}/${SettingsPath.ProfilePage}`;
}
if (!isDefined(defaultObjectPathInfo)) { if (!isDefined(defaultObjectPathInfo)) {
return AppPath.NotFound; return AppPath.NotFound;
} }
@ -91,7 +116,11 @@ export const useDefaultHomePagePath = () => {
{ objectNamePlural: namePlural }, { objectNamePlural: namePlural },
viewId ? { viewId } : undefined, viewId ? { viewId } : undefined,
); );
}, [currentUser, defaultObjectPathInfo]); }, [
currentUser,
defaultObjectPathInfo,
readableAlphaSortedActiveNonSystemObjectMetadataItems,
]);
return { defaultHomePagePath }; return { defaultHomePagePath };
}; };

View File

@ -5,6 +5,8 @@ import { getActionMenuIdFromRecordIndexId } from '@/action-menu/utils/getActionM
import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId'; import { MAIN_CONTEXT_STORE_INSTANCE_ID } from '@/context-store/constants/MainContextStoreInstanceId';
import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow'; import { useContextStoreObjectMetadataItemOrThrow } from '@/context-store/hooks/useContextStoreObjectMetadataItemOrThrow';
import { contextStoreCurrentViewIdComponentState } from '@/context-store/states/contextStoreCurrentViewIdComponentState'; 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 { lastShowPageRecordIdState } from '@/object-record/record-field/states/lastShowPageRecordId';
import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext'; import { RecordFilterGroupsComponentInstanceContext } from '@/object-record/record-filter-group/states/context/RecordFilterGroupsComponentInstanceContext';
import { RecordFiltersComponentInstanceContext } from '@/object-record/record-filter/states/context/RecordFiltersComponentInstanceContext'; 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 styled from '@emotion/styled';
import { useRecoilCallback } from 'recoil'; import { useRecoilCallback } from 'recoil';
import { capitalize } from 'twenty-shared/utils'; import { capitalize } from 'twenty-shared/utils';
import { getObjectPermissionsForObject } from '@/object-metadata/utils/getObjectPermissionsForObject'; import { NotFound } from '~/pages/not-found/NotFound';
import { useObjectPermissions } from '@/object-record/hooks/useObjectPermissions';
const StyledIndexContainer = styled.div` const StyledIndexContainer = styled.div`
display: flex; display: flex;
@ -68,7 +69,7 @@ export const RecordIndexContainerGater = () => {
const hasObjectReadPermissions = objectPermissions.canReadObjectRecords; const hasObjectReadPermissions = objectPermissions.canReadObjectRecords;
if (!hasObjectReadPermissions) { if (!hasObjectReadPermissions) {
return <></>; return <NotFound />;
} }
return ( return (

View File

@ -128,6 +128,12 @@ export const mockedUserData: MockedUser = {
currentWorkspace: mockCurrentWorkspace, currentWorkspace: mockCurrentWorkspace,
currentUserWorkspace: { currentUserWorkspace: {
settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS], settingsPermissions: [SettingPermissionType.WORKSPACE_MEMBERS],
objectPermissions: [
{
objectMetadataId: '4a45f524-b8cb-40e8-8450-28e402b442cf',
canReadObjectRecords: true,
},
],
}, },
locale: 'en', locale: 'en',
workspaces: [{ workspace: mockCurrentWorkspace }], workspaces: [{ workspace: mockCurrentWorkspace }],