[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:
@ -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();
|
||||||
},
|
},
|
||||||
|
|||||||
@ -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 };
|
||||||
};
|
};
|
||||||
|
|||||||
@ -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 (
|
||||||
|
|||||||
@ -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 }],
|
||||||
|
|||||||
Reference in New Issue
Block a user