[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 { 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();
},

View File

@ -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<ObjectPathInfo | null>(() => {
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<ObjectPathInfo | null>(() => {
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 };
};

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 { 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 <NotFound />;
}
return (

View File

@ -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 }],