refacto(*): remove everything about default workspace (#9157)

## Summary
- [x] Remove defaultWorkspace in user
- [x] Remove all occurrence of defaultWorkspace and defaultWorkspaceId
- [x] Improve activate workspace flow
- [x] Improve security on social login
- [x] Add `ImpersonateGuard`
- [x] Allow to use impersonation with couple `User/Workspace`
- [x] Prevent unexpected reload on activate workspace
- [x] Scope login token with workspaceId 

Fix https://github.com/twentyhq/twenty/issues/9033#event-15714863042
This commit is contained in:
Antoine Moreaux
2024-12-24 12:47:41 +01:00
committed by GitHub
parent fe6948ba0b
commit cd2946b670
78 changed files with 1150 additions and 1244 deletions

View File

@ -2,16 +2,12 @@ import styled from '@emotion/styled';
import { zodResolver } from '@hookform/resolvers/zod';
import { useCallback } from 'react';
import { Controller, SubmitHandler, useForm } from 'react-hook-form';
import { useSetRecoilState, useRecoilValue } from 'recoil';
import { Key } from 'ts-key-enum';
import { H2Title, Loader, MainButton } from 'twenty-ui';
import { z } from 'zod';
import { SubTitle } from '@/auth/components/SubTitle';
import { Title } from '@/auth/components/Title';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { FIND_MANY_OBJECT_METADATA_ITEMS } from '@/object-metadata/graphql/queries';
import { useApolloMetadataClient } from '@/object-metadata/hooks/useApolloMetadataClient';
import { useOnboardingStatus } from '@/onboarding/hooks/useOnboardingStatus';
import { WorkspaceLogoUploader } from '@/settings/workspace/components/WorkspaceLogoUploader';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
@ -22,9 +18,7 @@ import {
useActivateWorkspaceMutation,
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { AppPath } from '@/types/AppPath';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { useAuth } from '@/auth/hooks/useAuth';
const StyledContentContainer = styled.div`
width: 100%;
@ -50,12 +44,9 @@ type Form = z.infer<typeof validationSchema>;
export const CreateWorkspace = () => {
const { enqueueSnackBar } = useSnackBar();
const onboardingStatus = useOnboardingStatus();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const { loadCurrentUser } = useAuth();
const [activateWorkspace] = useActivateWorkspaceMutation();
const apolloMetadataClient = useApolloMetadataClient();
const setIsCurrentUserLoaded = useSetRecoilState(isCurrentUserLoadedState);
// Form
const {
@ -81,39 +72,17 @@ export const CreateWorkspace = () => {
},
});
setIsCurrentUserLoaded(false);
if (isDefined(result.data) && isMultiWorkspaceEnabled) {
return redirectToWorkspaceDomain(
result.data.activateWorkspace.workspace.subdomain,
AppPath.Verify,
{
loginToken: result.data.activateWorkspace.loginToken.token,
},
);
}
await apolloMetadataClient?.refetchQueries({
include: [FIND_MANY_OBJECT_METADATA_ITEMS],
});
if (isDefined(result.errors)) {
throw result.errors ?? new Error('Unknown error');
}
await loadCurrentUser();
} catch (error: any) {
enqueueSnackBar(error?.message, {
variant: SnackBarVariant.Error,
});
}
},
[
activateWorkspace,
setIsCurrentUserLoaded,
isMultiWorkspaceEnabled,
apolloMetadataClient,
redirectToWorkspaceDomain,
enqueueSnackBar,
],
[activateWorkspace, enqueueSnackBar, loadCurrentUser],
);
const handleKeyDown = (event: React.KeyboardEvent<HTMLInputElement>) => {

View File

@ -1,14 +1,10 @@
import { SettingsAdminImpersonateUsers } from '@/settings/admin-panel/components/SettingsAdminImpersonateUsers';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { useTheme } from '@emotion/react';
import { IconFlag, UndecoratedLink } from 'twenty-ui';
import { SettingsAdminContent } from '@/settings/admin-panel/components/SettingsAdminContent';
export const SettingsAdmin = () => {
const theme = useTheme();
return (
<SubMenuTopBarContainer
title="Server Admin Panel"
@ -21,18 +17,7 @@ export const SettingsAdmin = () => {
]}
>
<SettingsPageContainer>
<SettingsAdminImpersonateUsers />
<UndecoratedLink to={getSettingsPagePath(SettingsPath.FeatureFlags)}>
<SettingsCard
Icon={
<IconFlag
size={theme.icon.size.lg}
stroke={theme.icon.stroke.sm}
/>
}
title="Feature Flags"
/>
</UndecoratedLink>
<SettingsAdminContent />
</SettingsPageContainer>
</SubMenuTopBarContainer>
);

View File

@ -1,243 +0,0 @@
import { SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID } from '@/settings/admin-panel/constants/SettingsAdminFeatureFlagsTabs';
import { useFeatureFlagsManagement } from '@/settings/admin-panel/hooks/useFeatureFlagsManagement';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { getSettingsPagePath } from '@/settings/utils/getSettingsPagePath';
import { SettingsPath } from '@/types/SettingsPath';
import { TextInput } from '@/ui/input/components/TextInput';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { TabList } from '@/ui/layout/tab/components/TabList';
import { useTabList } from '@/ui/layout/tab/hooks/useTabList';
import { Table } from '@/ui/layout/table/components/Table';
import { TableCell } from '@/ui/layout/table/components/TableCell';
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
import { TableRow } from '@/ui/layout/table/components/TableRow';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import styled from '@emotion/styled';
import { isNonEmptyString } from '@sniptt/guards';
import { useState } from 'react';
import { getImageAbsoluteURI } from 'twenty-shared';
import {
Button,
H1Title,
H1TitleFontColor,
H2Title,
IconSearch,
isDefined,
Section,
Toggle,
} from 'twenty-ui';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
const StyledLinkContainer = styled.div`
margin-right: ${({ theme }) => theme.spacing(2)};
width: 100%;
`;
const StyledContainer = styled.div`
align-items: center;
display: flex;
flex-direction: row;
`;
const StyledErrorSection = styled.div`
color: ${({ theme }) => theme.font.color.danger};
margin-top: ${({ theme }) => theme.spacing(2)};
`;
const StyledUserInfo = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(5)};
`;
const StyledTable = styled(Table)`
margin-top: ${({ theme }) => theme.spacing(0.5)};
`;
const StyledTabListContainer = styled.div`
align-items: center;
border-bottom: ${({ theme }) => `1px solid ${theme.border.color.light}`};
box-sizing: border-box;
display: flex;
gap: ${({ theme }) => theme.spacing(2)};
`;
const StyledContentContainer = styled.div`
flex: 1;
width: 100%;
padding: ${({ theme }) => theme.spacing(4)} 0;
`;
export const SettingsAdminFeatureFlags = () => {
const [userIdentifier, setUserIdentifier] = useState('');
const { activeTabId, setActiveTabId } = useTabList(
SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID,
);
const {
userLookupResult,
handleUserLookup,
handleFeatureFlagUpdate,
isLoading,
error,
} = useFeatureFlagsManagement();
const handleSearch = async () => {
setActiveTabId('');
const result = await handleUserLookup(userIdentifier);
if (
isDefined(result?.workspaces) &&
result.workspaces.length > 0 &&
!error
) {
setActiveTabId(result.workspaces[0].id);
}
};
const shouldShowUserData = userLookupResult && !error;
const activeWorkspace = userLookupResult?.workspaces.find(
(workspace) => workspace.id === activeTabId,
);
const tabs =
userLookupResult?.workspaces.map((workspace) => ({
id: workspace.id,
title: workspace.name,
logo:
getImageAbsoluteURI({
imageUrl: isNonEmptyString(workspace.logo)
? workspace.logo
: DEFAULT_WORKSPACE_LOGO,
baseUrl: REACT_APP_SERVER_BASE_URL,
}) ?? '',
})) ?? [];
const renderWorkspaceContent = () => {
if (!activeWorkspace) return null;
return (
<>
<H2Title title={activeWorkspace.name} description={'Workspace Name'} />
<H2Title
title={`${activeWorkspace.totalUsers} ${
activeWorkspace.totalUsers > 1 ? 'Users' : 'User'
}`}
description={'Total Users'}
/>
<StyledTable>
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
>
<TableHeader>Feature Flag</TableHeader>
<TableHeader align="right">Status</TableHeader>
</TableRow>
{activeWorkspace.featureFlags.map((flag) => (
<TableRow
gridAutoColumns="1fr 100px"
mobileGridAutoColumns="1fr 80px"
key={flag.key}
>
<TableCell>{flag.key}</TableCell>
<TableCell align="right">
<Toggle
value={flag.value}
onChange={(newValue) =>
handleFeatureFlagUpdate(
activeWorkspace.id,
flag.key,
newValue,
)
}
/>
</TableCell>
</TableRow>
))}
</StyledTable>
</>
);
};
return (
<SubMenuTopBarContainer
title="Feature Flags"
links={[
{
children: 'Other',
href: getSettingsPagePath(SettingsPath.AdminPanel),
},
{
children: 'Server Admin Panel',
href: getSettingsPagePath(SettingsPath.AdminPanel),
},
{ children: 'Feature Flags' },
]}
>
<SettingsPageContainer>
<Section>
<H2Title
title="Feature Flags Management"
description="Look up users and manage their workspace feature flags."
/>
<StyledContainer>
<StyledLinkContainer>
<TextInput
value={userIdentifier}
onChange={setUserIdentifier}
onInputEnter={handleSearch}
placeholder="Enter user ID or email address"
fullWidth
disabled={isLoading}
/>
</StyledLinkContainer>
<Button
Icon={IconSearch}
variant="primary"
accent="blue"
title="Search"
onClick={handleSearch}
disabled={!userIdentifier.trim() || isLoading}
/>
</StyledContainer>
{error && <StyledErrorSection>{error}</StyledErrorSection>}
</Section>
{shouldShowUserData && (
<Section>
<StyledUserInfo>
<H1Title title="User Info" fontColor={H1TitleFontColor.Primary} />
<H2Title
title={`${userLookupResult.user.firstName || ''} ${
userLookupResult.user.lastName || ''
}`.trim()}
description="User Name"
/>
<H2Title
title={userLookupResult.user.email}
description="User Email"
/>
<H2Title title={userLookupResult.user.id} description="User ID" />
</StyledUserInfo>
<H1Title title="Workspaces" fontColor={H1TitleFontColor.Primary} />
<StyledTabListContainer>
<TabList
tabs={tabs}
tabListInstanceId={SETTINGS_ADMIN_FEATURE_FLAGS_TAB_ID}
behaveAsLinks={false}
/>
</StyledTabListContainer>
<StyledContentContainer>
{renderWorkspaceContent()}
</StyledContentContainer>
</Section>
)}
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};