Files
twenty/packages/twenty-front/src/modules/users/hooks/useLoadCurrentUser.ts
T
Félix Malfait cff17db6cb Enhance role-check system with stricter checks (#15392)
## Overview

This PR strengthens our permission system by introducing more granular
role-based access control across the platform.

## Changes

### New Permissions Added
- **Applications** - Control who can install and manage applications
- **Layouts** - Control who can customize page layouts and UI structure
- **AI** - Control access to AI features and agents
- **Upload File** - Separate permission for file uploads
- **Download File** - Separate permission for file downloads (frontend
visibility)

### Security Enhancements
- Implemented whitelist-based validation for workspace field updates
- Added explicit permission guards to core entity resolvers
- Enhanced ESLint rule to enforce permission checks on all mutations
- Created `CustomPermissionGuard` and `NoPermissionGuard` for better
code documentation

### Affected Components
- Core entity resolvers: webhooks, files, domains, applications,
layouts, postgres credentials
- Workspace update mutations now use whitelist validation
- Settings UI updated with new permission controls

### Developer Experience
- ESLint now catches missing permission guards during development
- Explicit guard markers make permission requirements clear in code
review
- Comprehensive test coverage for new permission logic

## Testing
-  All TypeScript type checks pass
-  ESLint validation passes
-  New permission guards properly enforced
-  Frontend UI displays new permissions correctly

## Migration Notes
Existing workspaces will need to assign the new permissions to roles as
needed. By default, all new permissions are set to `false` for non-admin
roles.
2025-11-07 15:37:17 +01:00

161 lines
5.6 KiB
TypeScript

import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
import { currentUserState } from '@/auth/states/currentUserState';
import { currentUserWorkspaceState } from '@/auth/states/currentUserWorkspaceState';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { useIsCurrentLocationOnAWorkspace } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspace';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { useInitializeFormatPreferences } from '@/localization/hooks/useInitializeFormatPreferences';
import { coreViewsState } from '@/views/states/coreViewState';
import { workspaceAuthBypassProvidersState } from '@/workspace/states/workspaceAuthBypassProvidersState';
import { useCallback } from 'react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { SOURCE_LOCALE, type APP_LOCALES } from 'twenty-shared/translations';
import { type ObjectPermissions } from 'twenty-shared/types';
import { isDefined } from 'twenty-shared/utils';
import { type ColorScheme } from 'twenty-ui/input';
import {
useFindAllCoreViewsLazyQuery,
useGetCurrentUserLazyQuery,
} from '~/generated-metadata/graphql';
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
export const useLoadCurrentUser = () => {
const setCurrentUser = useSetRecoilState(currentUserState);
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
const setCurrentUserWorkspace = useSetRecoilState(currentUserWorkspaceState);
const setCurrentWorkspaceMembers = useSetRecoilState(
currentWorkspaceMembersState,
);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const { initializeFormatPreferences } = useInitializeFormatPreferences();
const setCoreViews = useSetRecoilState(coreViewsState);
const setWorkspaceAuthBypassProviders = useSetRecoilState(
workspaceAuthBypassProvidersState,
);
const authProviders = useRecoilValue(authProvidersState);
const { isOnAWorkspace } = useIsCurrentLocationOnAWorkspace();
const [getCurrentUser] = useGetCurrentUserLazyQuery();
const [findAllCoreViews] = useFindAllCoreViewsLazyQuery();
const loadCurrentUser = useCallback(async () => {
const currentUserResult = await getCurrentUser({
fetchPolicy: 'network-only',
});
const coreViewsResult = await findAllCoreViews({
fetchPolicy: 'network-only',
});
if (isDefined(currentUserResult.error)) {
throw new Error(currentUserResult.error.message);
}
const user = currentUserResult.data?.currentUser;
if (!user) {
throw new Error('No current user result');
}
let workspaceMember = null;
setCurrentUser(user);
if (isDefined(user.workspaceMembers)) {
setCurrentWorkspaceMembers(user.workspaceMembers);
}
if (isDefined(user.availableWorkspaces)) {
setAvailableWorkspaces(user.availableWorkspaces);
}
if (isDefined(user.currentUserWorkspace)) {
setCurrentUserWorkspace({
permissionFlags: user.currentUserWorkspace.permissionFlags ?? [],
twoFactorAuthenticationMethodSummary:
user.currentUserWorkspace.twoFactorAuthenticationMethodSummary ?? [],
objectsPermissions:
(user.currentUserWorkspace.objectsPermissions as Array<
ObjectPermissions & { objectMetadataId: string }
>) ?? [],
});
}
if (isDefined(user.workspaceMember)) {
workspaceMember = {
...user.workspaceMember,
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
locale: user.workspaceMember?.locale ?? SOURCE_LOCALE,
};
setCurrentWorkspaceMember(workspaceMember);
// Initialize unified format preferences state
initializeFormatPreferences(workspaceMember);
dynamicActivate(
(workspaceMember.locale as keyof typeof APP_LOCALES) ?? SOURCE_LOCALE,
);
}
const workspace = user.currentWorkspace ?? null;
setCurrentWorkspace(workspace);
if (isDefined(workspace)) {
setWorkspaceAuthBypassProviders({
google: authProviders.google && workspace.isGoogleAuthBypassEnabled,
microsoft:
authProviders.microsoft && workspace.isMicrosoftAuthBypassEnabled,
password:
authProviders.password && workspace.isPasswordAuthBypassEnabled,
});
}
if (isDefined(workspace) && isOnAWorkspace) {
setLastAuthenticateWorkspaceDomain({
workspaceId: workspace.id,
workspaceUrl: getWorkspaceUrl(workspace.workspaceUrls),
});
}
if (isDefined(coreViewsResult.data?.getCoreViews)) {
setCoreViews(coreViewsResult.data.getCoreViews);
}
return {
user,
workspaceMember,
workspace,
};
}, [
getCurrentUser,
findAllCoreViews,
setCurrentUser,
setCurrentWorkspace,
isOnAWorkspace,
setCurrentWorkspaceMembers,
setAvailableWorkspaces,
setCurrentUserWorkspace,
setCurrentWorkspaceMember,
initializeFormatPreferences,
setLastAuthenticateWorkspaceDomain,
setCoreViews,
authProviders,
setWorkspaceAuthBypassProviders,
]);
return {
loadCurrentUser,
};
};