refacto(invite|signin): remove unused code + fix signin on invite page. (#9745)

- Replace `window.location.replace` by `useRedirect` hook.
- Remove unused code: `switchWorkspace, addUserByInviteHash...`
- Refacto `Invite` component.
- Fix signin on invite modal.
This commit is contained in:
Antoine Moreaux
2025-01-21 16:33:31 +01:00
committed by GitHub
parent 2e9a77f702
commit 34afd73923
28 changed files with 67 additions and 715 deletions

View File

@ -170,6 +170,7 @@ export type ClientConfig = {
frontDomain: Scalars['String']['output'];
isEmailVerificationRequired: Scalars['Boolean']['output'];
isMultiWorkspaceEnabled: Scalars['Boolean']['output'];
publicFeatureFlags: Array<PublicFeatureFlag>;
sentry: Sentry;
signInPrefilled: Scalars['Boolean']['output'];
support: Support;
@ -545,8 +546,6 @@ export type Mutation = {
__typename?: 'Mutation';
activateWorkflowVersion: Scalars['Boolean']['output'];
activateWorkspace: Workspace;
addUserToWorkspace: User;
addUserToWorkspaceByInviteToken: User;
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
@ -590,12 +589,12 @@ export type Mutation = {
sendInvitations: SendInvitationsOutput;
signUp: SignUpOutput;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
switchWorkspace: PublicWorkspaceDataOutput;
syncRemoteTable: RemoteTable;
syncRemoteTableSchemaChanges: RemoteTable;
track: Analytics;
unsyncRemoteTable: RemoteTable;
updateBillingSubscription: UpdateBillingEntity;
updateLabPublicFeatureFlag: Scalars['Boolean']['output'];
updateOneField: Field;
updateOneObject: Object;
updateOneRemoteServer: RemoteServer;
@ -623,16 +622,6 @@ export type MutationActivateWorkspaceArgs = {
};
export type MutationAddUserToWorkspaceArgs = {
inviteHash: Scalars['String']['input'];
};
export type MutationAddUserToWorkspaceByInviteTokenArgs = {
inviteToken: Scalars['String']['input'];
};
export type MutationAuthorizeAppArgs = {
clientId: Scalars['String']['input'];
codeChallenge?: InputMaybe<Scalars['String']['input']>;
@ -833,11 +822,6 @@ export type MutationSignUpArgs = {
};
export type MutationSwitchWorkspaceArgs = {
workspaceId: Scalars['String']['input'];
};
export type MutationSyncRemoteTableArgs = {
input: RemoteTableInput;
};
@ -859,6 +843,11 @@ export type MutationUnsyncRemoteTableArgs = {
};
export type MutationUpdateLabPublicFeatureFlagArgs = {
input: UpdateLabPublicFeatureFlagInput;
};
export type MutationUpdateOneFieldArgs = {
input: UpdateOneFieldMetadataInput;
};
@ -1007,6 +996,19 @@ export type ProductPricesEntity = {
totalNumberOfPrices: Scalars['Int']['output'];
};
export type PublicFeatureFlag = {
__typename?: 'PublicFeatureFlag';
key: FeatureFlagKey;
metadata: PublicFeatureFlagMetadata;
};
export type PublicFeatureFlagMetadata = {
__typename?: 'PublicFeatureFlagMetadata';
description: Scalars['String']['output'];
imagePath: Scalars['String']['output'];
label: Scalars['String']['output'];
};
export type PublicWorkspaceDataOutput = {
__typename?: 'PublicWorkspaceDataOutput';
authProviders: AuthProviders;
@ -1540,6 +1542,11 @@ export type UpdateFieldInput = {
settings?: InputMaybe<Scalars['JSON']['input']>;
};
export type UpdateLabPublicFeatureFlagInput = {
publicFeatureFlag: Scalars['String']['input'];
value: Scalars['Boolean']['input'];
};
export type UpdateObjectPayload = {
description?: InputMaybe<Scalars['String']['input']>;
icon?: InputMaybe<Scalars['String']['input']>;
@ -1713,7 +1720,7 @@ export type WorkflowRun = {
export type WorkflowVersion = {
__typename?: 'WorkflowVersion';
workflowVersionId: Scalars['UUID']['output'];
id: Scalars['UUID']['output'];
};
export type Workspace = {

View File

@ -471,8 +471,6 @@ export type Mutation = {
__typename?: 'Mutation';
activateWorkflowVersion: Scalars['Boolean'];
activateWorkspace: Workspace;
addUserToWorkspace: User;
addUserToWorkspaceByInviteToken: User;
authorizeApp: AuthorizeApp;
challenge: LoginToken;
checkoutSession: SessionEntity;
@ -512,7 +510,6 @@ export type Mutation = {
sendInvitations: SendInvitationsOutput;
signUp: SignUpOutput;
skipSyncEmailOnboardingStep: OnboardingStepSuccess;
switchWorkspace: PublicWorkspaceDataOutput;
track: Analytics;
updateBillingSubscription: UpdateBillingEntity;
updateLabPublicFeatureFlag: Scalars['Boolean'];
@ -542,16 +539,6 @@ export type MutationActivateWorkspaceArgs = {
};
export type MutationAddUserToWorkspaceArgs = {
inviteHash: Scalars['String'];
};
export type MutationAddUserToWorkspaceByInviteTokenArgs = {
inviteToken: Scalars['String'];
};
export type MutationAuthorizeAppArgs = {
clientId: Scalars['String'];
codeChallenge?: InputMaybe<Scalars['String']>;
@ -722,11 +709,6 @@ export type MutationSignUpArgs = {
};
export type MutationSwitchWorkspaceArgs = {
workspaceId: Scalars['String'];
};
export type MutationTrackArgs = {
action: Scalars['String'];
payload: Scalars['JSON'];
@ -1966,13 +1948,6 @@ export type SignUpMutationVariables = Exact<{
export type SignUpMutation = { __typename?: 'Mutation', signUp: { __typename?: 'SignUpOutput', loginToken: { __typename?: 'AuthToken', token: string, expiresAt: string }, workspace: { __typename?: 'WorkspaceSubdomainAndId', id: string, subdomain: string } } };
export type SwitchWorkspaceMutationVariables = Exact<{
workspaceId: Scalars['String'];
}>;
export type SwitchWorkspaceMutation = { __typename?: 'Mutation', switchWorkspace: { __typename?: 'PublicWorkspaceDataOutput', id: string, subdomain: string, authProviders: { __typename?: 'AuthProviders', google: boolean, magicLink: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> } } };
export type UpdatePasswordViaResetTokenMutationVariables = Exact<{
token: Scalars['String'];
newPassword: Scalars['String'];
@ -2206,20 +2181,6 @@ export type GetWorkspaceInvitationsQuery = { __typename?: 'Query', findWorkspace
export type WorkspaceMemberQueryFragmentFragment = { __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } };
export type AddUserToWorkspaceMutationVariables = Exact<{
inviteHash: Scalars['String'];
}>;
export type AddUserToWorkspaceMutation = { __typename?: 'Mutation', addUserToWorkspace: { __typename?: 'User', id: any } };
export type AddUserToWorkspaceByInviteTokenMutationVariables = Exact<{
inviteToken: Scalars['String'];
}>;
export type AddUserToWorkspaceByInviteTokenMutation = { __typename?: 'Mutation', addUserToWorkspaceByInviteToken: { __typename?: 'User', id: any } };
export type ActivateWorkspaceMutationVariables = Exact<{
input: ActivateWorkspaceInput;
}>;
@ -2251,7 +2212,7 @@ export type GetWorkspaceFromInviteHashQueryVariables = Exact<{
}>;
export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, allowImpersonation: boolean } };
export type GetWorkspaceFromInviteHashQuery = { __typename?: 'Query', findWorkspaceFromInviteHash: { __typename?: 'Workspace', id: any, displayName?: string | null, logo?: string | null, allowImpersonation: boolean, subdomain: string } };
export const TimelineCalendarEventParticipantFragmentFragmentDoc = gql`
fragment TimelineCalendarEventParticipantFragment on TimelineCalendarEventParticipant {
@ -3106,53 +3067,6 @@ export function useSignUpMutation(baseOptions?: Apollo.MutationHookOptions<SignU
export type SignUpMutationHookResult = ReturnType<typeof useSignUpMutation>;
export type SignUpMutationResult = Apollo.MutationResult<SignUpMutation>;
export type SignUpMutationOptions = Apollo.BaseMutationOptions<SignUpMutation, SignUpMutationVariables>;
export const SwitchWorkspaceDocument = gql`
mutation SwitchWorkspace($workspaceId: String!) {
switchWorkspace(workspaceId: $workspaceId) {
id
subdomain
authProviders {
sso {
id
name
type
status
issuer
}
google
magicLink
password
microsoft
}
}
}
`;
export type SwitchWorkspaceMutationFn = Apollo.MutationFunction<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>;
/**
* __useSwitchWorkspaceMutation__
*
* To run a mutation, you first call `useSwitchWorkspaceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useSwitchWorkspaceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [switchWorkspaceMutation, { data, loading, error }] = useSwitchWorkspaceMutation({
* variables: {
* workspaceId: // value for 'workspaceId'
* },
* });
*/
export function useSwitchWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>(SwitchWorkspaceDocument, options);
}
export type SwitchWorkspaceMutationHookResult = ReturnType<typeof useSwitchWorkspaceMutation>;
export type SwitchWorkspaceMutationResult = Apollo.MutationResult<SwitchWorkspaceMutation>;
export type SwitchWorkspaceMutationOptions = Apollo.BaseMutationOptions<SwitchWorkspaceMutation, SwitchWorkspaceMutationVariables>;
export const UpdatePasswordViaResetTokenDocument = gql`
mutation UpdatePasswordViaResetToken($token: String!, $newPassword: String!) {
updatePasswordViaResetToken(
@ -4454,72 +4368,6 @@ export function useGetWorkspaceInvitationsLazyQuery(baseOptions?: Apollo.LazyQue
export type GetWorkspaceInvitationsQueryHookResult = ReturnType<typeof useGetWorkspaceInvitationsQuery>;
export type GetWorkspaceInvitationsLazyQueryHookResult = ReturnType<typeof useGetWorkspaceInvitationsLazyQuery>;
export type GetWorkspaceInvitationsQueryResult = Apollo.QueryResult<GetWorkspaceInvitationsQuery, GetWorkspaceInvitationsQueryVariables>;
export const AddUserToWorkspaceDocument = gql`
mutation AddUserToWorkspace($inviteHash: String!) {
addUserToWorkspace(inviteHash: $inviteHash) {
id
}
}
`;
export type AddUserToWorkspaceMutationFn = Apollo.MutationFunction<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>;
/**
* __useAddUserToWorkspaceMutation__
*
* To run a mutation, you first call `useAddUserToWorkspaceMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useAddUserToWorkspaceMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [addUserToWorkspaceMutation, { data, loading, error }] = useAddUserToWorkspaceMutation({
* variables: {
* inviteHash: // value for 'inviteHash'
* },
* });
*/
export function useAddUserToWorkspaceMutation(baseOptions?: Apollo.MutationHookOptions<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>(AddUserToWorkspaceDocument, options);
}
export type AddUserToWorkspaceMutationHookResult = ReturnType<typeof useAddUserToWorkspaceMutation>;
export type AddUserToWorkspaceMutationResult = Apollo.MutationResult<AddUserToWorkspaceMutation>;
export type AddUserToWorkspaceMutationOptions = Apollo.BaseMutationOptions<AddUserToWorkspaceMutation, AddUserToWorkspaceMutationVariables>;
export const AddUserToWorkspaceByInviteTokenDocument = gql`
mutation AddUserToWorkspaceByInviteToken($inviteToken: String!) {
addUserToWorkspaceByInviteToken(inviteToken: $inviteToken) {
id
}
}
`;
export type AddUserToWorkspaceByInviteTokenMutationFn = Apollo.MutationFunction<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>;
/**
* __useAddUserToWorkspaceByInviteTokenMutation__
*
* To run a mutation, you first call `useAddUserToWorkspaceByInviteTokenMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useAddUserToWorkspaceByInviteTokenMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [addUserToWorkspaceByInviteTokenMutation, { data, loading, error }] = useAddUserToWorkspaceByInviteTokenMutation({
* variables: {
* inviteToken: // value for 'inviteToken'
* },
* });
*/
export function useAddUserToWorkspaceByInviteTokenMutation(baseOptions?: Apollo.MutationHookOptions<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>(AddUserToWorkspaceByInviteTokenDocument, options);
}
export type AddUserToWorkspaceByInviteTokenMutationHookResult = ReturnType<typeof useAddUserToWorkspaceByInviteTokenMutation>;
export type AddUserToWorkspaceByInviteTokenMutationResult = Apollo.MutationResult<AddUserToWorkspaceByInviteTokenMutation>;
export type AddUserToWorkspaceByInviteTokenMutationOptions = Apollo.BaseMutationOptions<AddUserToWorkspaceByInviteTokenMutation, AddUserToWorkspaceByInviteTokenMutationVariables>;
export const ActivateWorkspaceDocument = gql`
mutation ActivateWorkspace($input: ActivateWorkspaceInput!) {
activateWorkspace(data: $input) {
@ -4666,6 +4514,7 @@ export const GetWorkspaceFromInviteHashDocument = gql`
displayName
logo
allowImpersonation
subdomain
}
}
`;

View File

@ -69,7 +69,7 @@ export const useApolloFactory = (options: Partial<Options<any>> = {}) => {
setCurrentUser(null);
setCurrentWorkspaceMember(null);
setCurrentWorkspace(null);
setWorkspaces(null);
setWorkspaces([]);
if (
!isMatchingLocation(AppPath.Verify) &&
!isMatchingLocation(AppPath.SignInUp) &&

View File

@ -1,23 +0,0 @@
import { gql } from '@apollo/client';
export const SWITCH_WORKSPACE = gql`
mutation SwitchWorkspace($workspaceId: String!) {
switchWorkspace(workspaceId: $workspaceId) {
id
subdomain
authProviders {
sso {
id
name
type
status
issuer
}
google
magicLink
password
microsoft
}
}
}
`;

View File

@ -349,7 +349,7 @@ export const useAuth = () => {
[setIsVerifyPendingState, verify, setTokenPair, loadCurrentUser],
);
const handleCrendentialsSignIn = useCallback(
const handleCredentialsSignIn = useCallback(
async (email: string, password: string, captchaToken?: string) => {
const { loginToken } = await handleChallenge(
email,
@ -499,7 +499,7 @@ export const useAuth = () => {
clearSession,
signOut: handleSignOut,
signUpWithCredentials: handleCredentialsSignUp,
signInWithCredentials: handleCrendentialsSignIn,
signInWithCredentials: handleCredentialsSignIn,
signInWithGoogle: handleGoogleLogin,
signInWithMicrosoft: handleMicrosoftLogin,
};

View File

@ -5,9 +5,9 @@ import { Workspace } from '~/generated/graphql';
export type Workspaces = Pick<
Workspace,
'id' | 'logo' | 'displayName' | 'subdomain'
>;
>[];
export const workspacesState = createState<Workspaces[] | null>({
export const workspacesState = createState<Workspaces>({
key: 'workspacesState',
defaultValue: [],
});

View File

@ -8,6 +8,7 @@ import {
} from '~/generated-metadata/graphql';
import { useCheckoutSessionMutation } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
export const useHandleCheckoutSession = ({
recurringInterval,
@ -18,6 +19,8 @@ export const useHandleCheckoutSession = ({
plan: BillingPlanKey;
requirePaymentMethod: boolean;
}) => {
const { redirect } = useRedirect();
const { enqueueSnackBar } = useSnackBar();
const [checkoutSession] = useCheckoutSessionMutation();
@ -44,7 +47,7 @@ export const useHandleCheckoutSession = ({
);
return;
}
window.location.replace(data.checkoutSession.url);
redirect(data.checkoutSession.url);
};
return { isSubmitting, handleCheckoutSession };
};

View File

@ -6,13 +6,13 @@ export const useBuildWorkspaceUrl = () => {
const domainConfiguration = useRecoilValue(domainConfigurationState);
const buildWorkspaceUrl = (
subdomain?: string,
subdomain: string,
pathname?: string,
searchParams?: Record<string, string>,
) => {
const url = new URL(window.location.href);
if (isDefined(subdomain) && subdomain.length !== 0) {
if (subdomain.length !== 0) {
url.hostname = `${subdomain}.${domainConfiguration.frontDomain}`;
}

View File

@ -1,3 +1,4 @@
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
import { InformationBanner } from '@/information-banner/components/InformationBanner';
import { SettingsPath } from '@/types/SettingsPath';
import { isDefined } from 'twenty-ui';
@ -5,6 +6,8 @@ import { useBillingPortalSessionQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
export const InformationBannerBillingSubscriptionPaused = () => {
const { redirect } = useRedirect();
const { data, loading } = useBillingPortalSessionQuery({
variables: {
returnUrlPath: getSettingsPath(SettingsPath.Billing),
@ -13,7 +16,7 @@ export const InformationBannerBillingSubscriptionPaused = () => {
const openBillingPortal = () => {
if (isDefined(data) && isDefined(data.billingPortalSession.url)) {
window.location.replace(data.billingPortalSession.url);
redirect(data.billingPortalSession.url);
}
};

View File

@ -3,8 +3,11 @@ import { SettingsPath } from '@/types/SettingsPath';
import { isDefined } from 'twenty-ui';
import { useBillingPortalSessionQuery } from '~/generated/graphql';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
export const InformationBannerFailPaymentInfo = () => {
const { redirect } = useRedirect();
const { data, loading } = useBillingPortalSessionQuery({
variables: {
returnUrlPath: getSettingsPath(SettingsPath.Billing),
@ -13,7 +16,7 @@ export const InformationBannerFailPaymentInfo = () => {
const openBillingPortal = () => {
if (isDefined(data) && isDefined(data.billingPortalSession.url)) {
window.location.replace(data.billingPortalSession.url);
redirect(data.billingPortalSession.url);
}
};

View File

@ -3,16 +3,13 @@ import { Workspaces } from '@/auth/states/workspaces';
import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { NavigationDrawerAnimatedCollapseWrapper } from '@/ui/navigation/navigation-drawer/components/NavigationDrawerAnimatedCollapseWrapper';
import { DEFAULT_WORKSPACE_LOGO } from '@/ui/navigation/navigation-drawer/constants/DefaultWorkspaceLogo';
import { MULTI_WORKSPACE_DROPDOWN_ID } from '@/ui/navigation/navigation-drawer/constants/MulitWorkspaceDropdownId';
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
import { NavigationDrawerHotKeyScope } from '@/ui/navigation/navigation-drawer/types/NavigationDrawerHotKeyScope';
import { isNavigationDrawerExpandedState } from '@/ui/navigation/states/isNavigationDrawerExpanded';
import { useTheme } from '@emotion/react';
import styled from '@emotion/styled';
import { useState } from 'react';
import { useRecoilState, useRecoilValue } from 'recoil';
import {
Avatar,
@ -20,6 +17,7 @@ import {
MenuItemSelectAvatar,
UndecoratedLink,
} from 'twenty-ui';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
const StyledContainer = styled.div<{ isNavigationDrawerExpanded: boolean }>`
align-items: center;
@ -56,7 +54,7 @@ const StyledIconChevronDown = styled(IconChevronDown)<{ disabled?: boolean }>`
`;
type MultiWorkspaceDropdownButtonProps = {
workspaces: Workspaces[];
workspaces: Workspaces;
};
export const MultiWorkspaceDropdownButton = ({
@ -64,19 +62,12 @@ export const MultiWorkspaceDropdownButton = ({
}: MultiWorkspaceDropdownButtonProps) => {
const theme = useTheme();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const [isMultiWorkspaceDropdownOpen, setToggleMultiWorkspaceDropdown] =
useState(false);
const { switchWorkspace } = useWorkspaceSwitching();
const { buildWorkspaceUrl } = useBuildWorkspaceUrl();
const { closeDropdown } = useDropdown(MULTI_WORKSPACE_DROPDOWN_ID);
const handleChange = async (workspaceId: string) => {
setToggleMultiWorkspaceDropdown(!isMultiWorkspaceDropdownOpen);
closeDropdown();
await switchWorkspace(workspaceId);
const handleChange = async (workspace: Workspaces[0]) => {
redirectToWorkspaceDomain(workspace.subdomain);
};
const [isNavigationDrawerExpanded] = useRecoilState(
isNavigationDrawerExpandedState,
@ -116,7 +107,7 @@ export const MultiWorkspaceDropdownButton = ({
to={buildWorkspaceUrl(workspace.subdomain)}
onClick={(event) => {
event?.preventDefault();
handleChange(workspace.id);
handleChange(workspace);
}}
>
<MenuItemSelectAvatar

View File

@ -57,8 +57,7 @@ export const NavigationDrawerHeader = ({
isNavigationDrawerExpandedState,
);
const isMultiWorkspace =
isMultiWorkspaceEnabled && workspaces !== null && workspaces.length > 1;
const isMultiWorkspace = isMultiWorkspaceEnabled && workspaces.length > 1;
return (
<StyledContainer>

View File

@ -1,47 +0,0 @@
import { useRecoilValue } from 'recoil';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSwitchWorkspaceMutation } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
export const useWorkspaceSwitching = () => {
const [switchWorkspaceMutation] = useSwitchWorkspaceMutation();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const { enqueueSnackBar } = useSnackBar();
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
const switchWorkspace = async (workspaceId: string) => {
if (currentWorkspace?.id === workspaceId) return;
if (!isMultiWorkspaceEnabled) {
return enqueueSnackBar(
'Switching workspace is not available in single workspace mode',
{
variant: SnackBarVariant.Error,
},
);
}
const { data, errors } = await switchWorkspaceMutation({
variables: {
workspaceId,
},
});
if (isDefined(errors) || !isDefined(data?.switchWorkspace.subdomain)) {
return redirectToDefaultDomain();
}
redirectToWorkspaceDomain(data.switchWorkspace.subdomain);
};
return { switchWorkspace };
};

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const ADD_USER_TO_WORKSPACE = gql`
mutation AddUserToWorkspace($inviteHash: String!) {
addUserToWorkspace(inviteHash: $inviteHash) {
id
}
}
`;

View File

@ -1,9 +0,0 @@
import { gql } from '@apollo/client';
export const ADD_USER_TO_WORKSPACE_BY_INVITE_TOKEN = gql`
mutation AddUserToWorkspaceByInviteToken($inviteToken: String!) {
addUserToWorkspaceByInviteToken(inviteToken: $inviteToken) {
id
}
}
`;

View File

@ -7,6 +7,7 @@ export const GET_WORKSPACE_FROM_INVITE_HASH = gql`
displayName
logo
allowImpersonation
subdomain
}
}
`;

View File

@ -1,104 +1,27 @@
import { Logo } from '@/auth/components/Logo';
import { Title } from '@/auth/components/Title';
import { FooterNote } from '@/auth/sign-in-up/components/FooterNote';
import { SignInUpWorkspaceScopeForm } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeForm';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useWorkspaceFromInviteHash } from '@/auth/sign-in-up/hooks/useWorkspaceFromInviteHash';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { useWorkspaceSwitching } from '@/ui/navigation/navigation-drawer/hooks/useWorkspaceSwitching';
import styled from '@emotion/styled';
import { useMemo } from 'react';
import { useSearchParams } from 'react-router-dom';
import { useRecoilValue } from 'recoil';
import { AnimatedEaseIn, Loader, MainButton } from 'twenty-ui';
import {
useAddUserToWorkspaceByInviteTokenMutation,
useAddUserToWorkspaceMutation,
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { currentUserState } from '@/auth/states/currentUserState';
import { AnimatedEaseIn } from 'twenty-ui';
import { SignInUpWorkspaceScopeFormEffect } from '@/auth/sign-in-up/components/SignInUpWorkspaceScopeFormEffect';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
margin-top: ${({ theme }) => theme.spacing(4)};
`;
export const Invite = () => {
const { workspace: workspaceFromInviteHash, workspaceInviteHash } =
useWorkspaceFromInviteHash();
const { form } = useSignInUpForm();
const currentWorkspace = useRecoilValue(currentWorkspaceState);
const currentUser = useRecoilValue(currentUserState);
const [addUserToWorkspace] = useAddUserToWorkspaceMutation();
const [addUserToWorkspaceByInviteToken] =
useAddUserToWorkspaceByInviteTokenMutation();
const { switchWorkspace } = useWorkspaceSwitching();
const [searchParams] = useSearchParams();
const workspaceInviteToken = searchParams.get('inviteToken');
const { workspace: workspaceFromInviteHash } = useWorkspaceFromInviteHash();
const title = useMemo(() => {
return `Join ${workspaceFromInviteHash?.displayName ?? ''} team`;
}, [workspaceFromInviteHash?.displayName]);
const handleUserJoinWorkspace = async () => {
if (isDefined(workspaceInviteToken) && isDefined(workspaceFromInviteHash)) {
await addUserToWorkspaceByInviteToken({
variables: {
inviteToken: workspaceInviteToken,
},
});
} else if (
isDefined(workspaceInviteHash) &&
isDefined(workspaceFromInviteHash)
) {
await addUserToWorkspace({
variables: {
inviteHash: workspaceInviteHash,
},
});
} else {
return;
}
await switchWorkspace(workspaceFromInviteHash.id);
};
if (
!isDefined(workspaceFromInviteHash) ||
(isDefined(workspaceFromInviteHash) &&
isDefined(currentWorkspace) &&
workspaceFromInviteHash.id === currentWorkspace.id)
) {
return <></>;
}
return (
<>
<AnimatedEaseIn>
<Logo secondaryLogo={workspaceFromInviteHash?.logo} />
</AnimatedEaseIn>
<Title animate>{title}</Title>
{isDefined(currentUser) ? (
<>
<StyledContentContainer>
<MainButton
title="Continue"
type="submit"
onClick={handleUserJoinWorkspace}
Icon={() => form.formState.isSubmitting && <Loader />}
fullWidth
/>
</StyledContentContainer>
<FooterNote />
</>
) : (
<>
<SignInUpWorkspaceScopeFormEffect />
<SignInUpWorkspaceScopeForm />
</>
)}
<SignInUpWorkspaceScopeFormEffect />
<SignInUpWorkspaceScopeForm />
</>
);
};

View File

@ -27,6 +27,7 @@ import {
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { useRedirect } from '@/domain-manager/hooks/useRedirect';
type SwitchInfo = {
newInterval: SubscriptionInterval;
@ -38,6 +39,8 @@ type SwitchInfo = {
export const SettingsBilling = () => {
const { t } = useLingui();
const { redirect } = useRedirect();
const MONTHLY_SWITCH_INFO: SwitchInfo = {
newInterval: SubscriptionInterval.Year,
to: t`to yearly`,
@ -89,7 +92,7 @@ export const SettingsBilling = () => {
const openBillingPortal = () => {
if (isDefined(data) && isDefined(data.billingPortalSession.url)) {
window.location.replace(data.billingPortalSession.url);
redirect(data.billingPortalSession.url);
}
};

View File

@ -17,7 +17,6 @@ import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/micr
// import { OAuthService } from 'src/engine/core-modules/auth/services/oauth.service';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-up.service';
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
@ -103,7 +102,6 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy';
RefreshTokenService,
LoginTokenService,
ResetPasswordService,
SwitchWorkspaceService,
TransientTokenService,
ApiKeyService,
SocialSsoService,

View File

@ -16,7 +16,6 @@ import { ApiKeyService } from './services/api-key.service';
import { AuthService } from './services/auth.service';
// import { OAuthService } from './services/oauth.service';
import { ResetPasswordService } from './services/reset-password.service';
import { SwitchWorkspaceService } from './services/switch-workspace.service';
import { EmailVerificationTokenService } from './token/services/email-verification-token.service';
import { LoginTokenService } from './token/services/login-token.service';
import { RenewTokenService } from './token/services/renew-token.service';
@ -74,10 +73,6 @@ describe('AuthResolver', () => {
provide: LoginTokenService,
useValue: {},
},
{
provide: SwitchWorkspaceService,
useValue: {},
},
{
provide: TransientTokenService,
useValue: {},

View File

@ -25,9 +25,7 @@ import {
import { AvailableWorkspaceOutput } from 'src/engine/core-modules/auth/dto/available-workspaces.output';
import { GetLoginTokenFromEmailVerificationTokenInput } from 'src/engine/core-modules/auth/dto/get-login-token-from-email-verification-token.input';
import { SignUpOutput } from 'src/engine/core-modules/auth/dto/sign-up.output';
import { SwitchWorkspaceInput } from 'src/engine/core-modules/auth/dto/switch-workspace.input';
import { ResetPasswordService } from 'src/engine/core-modules/auth/services/reset-password.service';
import { SwitchWorkspaceService } from 'src/engine/core-modules/auth/services/switch-workspace.service';
import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service';
import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service';
import { RenewTokenService } from 'src/engine/core-modules/auth/token/services/renew-token.service';
@ -38,7 +36,6 @@ import { EmailVerificationService } from 'src/engine/core-modules/email-verifica
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
@ -70,7 +67,6 @@ export class AuthResolver {
private apiKeyService: ApiKeyService,
private resetPasswordService: ResetPasswordService,
private loginTokenService: LoginTokenService,
private switchWorkspaceService: SwitchWorkspaceService,
private transientTokenService: TransientTokenService,
private emailVerificationService: EmailVerificationService,
// private oauthService: OAuthService,
@ -307,18 +303,6 @@ export class AuthResolver {
);
}
@Mutation(() => PublicWorkspaceDataOutput)
@UseGuards(WorkspaceAuthGuard, UserAuthGuard)
async switchWorkspace(
@AuthUser() user: User,
@Args() args: SwitchWorkspaceInput,
): Promise<PublicWorkspaceDataOutput> {
return await this.switchWorkspaceService.switchWorkspace(
user,
args.workspaceId,
);
}
@Mutation(() => AuthTokens)
async renewToken(@Args() args: AppTokenInput): Promise<AuthTokens> {
const tokens = await this.renewTokenService.generateTokensFromRefreshToken(

View File

@ -12,9 +12,7 @@ export const PASSWORD_REGEX = /^.{8,}$/;
const saltRounds = 10;
export const hashPassword = async (password: string) => {
const hash = await bcrypt.hash(password, saltRounds);
return hash;
return await bcrypt.hash(password, saltRounds);
};
export const compareHash = async (password: string, passwordHash: string) => {

View File

@ -1,11 +0,0 @@
import { ArgsType, Field } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class SwitchWorkspaceInput {
@Field(() => String)
@IsNotEmpty()
@IsString()
workspaceId: string;
}

View File

@ -137,10 +137,7 @@ export class SignInUpService {
password: string;
passwordHash: string;
}) {
const isValid = await compareHash(
await this.generateHash(password),
passwordHash,
);
const isValid = await compareHash(password, passwordHash);
if (!isValid) {
throw new AuthException(

View File

@ -1,186 +0,0 @@
import { Test, TestingModule } from '@nestjs/testing';
import { getRepositoryToken } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { AuthException } from 'src/engine/core-modules/auth/auth.exception';
import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service';
import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { SwitchWorkspaceService } from './switch-workspace.service';
describe('SwitchWorkspaceService', () => {
let service: SwitchWorkspaceService;
let userRepository: Repository<User>;
let workspaceRepository: Repository<Workspace>;
beforeEach(async () => {
const module: TestingModule = await Test.createTestingModule({
providers: [
SwitchWorkspaceService,
{
provide: getRepositoryToken(User, 'core'),
useClass: Repository,
},
{
provide: getRepositoryToken(Workspace, 'core'),
useClass: Repository,
},
{
provide: AccessTokenService,
useValue: {
generateAccessToken: jest.fn(),
},
},
{
provide: RefreshTokenService,
useValue: {
generateRefreshToken: jest.fn(),
},
},
{
provide: EnvironmentService,
useValue: {
get: jest.fn(),
},
},
{
provide: UserService,
useValue: {},
},
],
}).compile();
service = module.get<SwitchWorkspaceService>(SwitchWorkspaceService);
userRepository = module.get<Repository<User>>(
getRepositoryToken(User, 'core'),
);
workspaceRepository = module.get<Repository<Workspace>>(
getRepositoryToken(Workspace, 'core'),
);
});
it('should be defined', () => {
expect(service).toBeDefined();
});
describe('switchWorkspace', () => {
it('should throw an error if user does not exist', async () => {
jest.spyOn(userRepository, 'findBy').mockResolvedValue([]);
jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(null);
await expect(
service.switchWorkspace(
{ id: 'non-existent-user' } as User,
'workspace-id',
),
).rejects.toThrow(AuthException);
});
it('should throw an error if workspace does not exist', async () => {
jest
.spyOn(userRepository, 'findBy')
.mockResolvedValue([{ id: 'user-id' } as User]);
jest.spyOn(workspaceRepository, 'findOne').mockResolvedValue(null);
await expect(
service.switchWorkspace(
{ id: 'user-id' } as User,
'non-existent-workspace',
),
).rejects.toThrow(AuthException);
});
it('should throw an error if user does not belong to workspace', async () => {
const mockUser = { id: 'user-id' };
const mockWorkspace = {
id: 'workspace-id',
workspaceUsers: [{ userId: 'other-user-id' }],
};
jest
.spyOn(userRepository, 'findBy')
.mockResolvedValue([mockUser as User]);
jest
.spyOn(workspaceRepository, 'findOne')
.mockResolvedValue(mockWorkspace as any);
await expect(
service.switchWorkspace(mockUser as User, 'workspace-id'),
).rejects.toThrow(AuthException);
});
it('should return SSO auth info if workspace has SSO providers', async () => {
const mockUser = { id: 'user-id' };
const mockWorkspace = {
id: 'workspace-id',
workspaceUsers: [{ userId: 'user-id' }],
logo: 'logo',
displayName: 'displayName',
isGoogleAuthEnabled: true,
isPasswordAuthEnabled: true,
isMicrosoftAuthEnabled: false,
workspaceSSOIdentityProviders: [
{
id: 'sso-id',
},
],
};
jest
.spyOn(userRepository, 'findBy')
.mockResolvedValue([mockUser as User]);
jest.spyOn(userRepository, 'save').mockResolvedValue(mockUser as User);
jest
.spyOn(workspaceRepository, 'findOne')
.mockResolvedValue(mockWorkspace as any);
const result = await service.switchWorkspace(
mockUser as User,
'workspace-id',
);
expect(result).toEqual({
id: mockWorkspace.id,
logo: expect.any(String),
displayName: expect.any(String),
authProviders: expect.any(Object),
});
});
it('should return workspace info if workspace does not have SSO providers', async () => {
const mockUser = { id: 'user-id' };
const mockWorkspace = {
id: 'workspace-id',
workspaceUsers: [{ userId: 'user-id' }],
workspaceSSOIdentityProviders: [],
logo: 'logo',
displayName: 'displayName',
};
jest
.spyOn(userRepository, 'findBy')
.mockResolvedValue([mockUser as User]);
jest
.spyOn(workspaceRepository, 'findOne')
.mockResolvedValue(mockWorkspace as any);
jest.spyOn(userRepository, 'save').mockResolvedValue({} as User);
const result = await service.switchWorkspace(
mockUser as User,
'workspace-id',
);
expect(result).toEqual({
id: mockWorkspace.id,
logo: expect.any(String),
displayName: expect.any(String),
authProviders: expect.any(Object),
});
});
});
});

View File

@ -1,66 +0,0 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data-output';
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
@Injectable()
export class SwitchWorkspaceService {
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly environmentService: EnvironmentService,
) {}
async switchWorkspace(user: User, workspaceId: string) {
const workspace = await this.workspaceRepository.findOne({
where: { id: workspaceId },
relations: ['workspaceUsers', 'workspaceSSOIdentityProviders'],
});
workspaceValidator.assertIsDefinedOrThrow(
workspace,
new AuthException('Workspace not found', AuthExceptionCode.INVALID_INPUT),
);
if (
!workspace.workspaceUsers
.map((userWorkspace) => userWorkspace.userId)
.includes(user.id)
) {
throw new AuthException(
'user does not belong to workspace',
AuthExceptionCode.FORBIDDEN_EXCEPTION,
);
}
const systemEnabledProviders: AuthProviders = {
google: this.environmentService.get('AUTH_GOOGLE_ENABLED'),
magicLink: false,
password: this.environmentService.get('AUTH_PASSWORD_ENABLED'),
microsoft: this.environmentService.get('AUTH_MICROSOFT_ENABLED'),
sso: [],
};
return {
id: workspace.id,
subdomain: workspace.subdomain,
logo: workspace.logo,
displayName: workspace.displayName,
authProviders: getAuthProvidersByWorkspace({
workspace,
systemEnabledProviders,
}),
};
}
}

View File

@ -1,18 +1,14 @@
import { UseGuards } from '@nestjs/common';
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { Resolver } from '@nestjs/graphql';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { WorkspaceInviteHashValidInput } from 'src/engine/core-modules/auth/dto/workspace-invite-hash.input';
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service';
import { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service';
import { WorkspaceInviteTokenInput } from 'src/engine/core-modules/auth/dto/workspace-invite-token.input';
@UseGuards(WorkspaceAuthGuard)
@Resolver(() => UserWorkspace)
@ -23,36 +19,4 @@ export class UserWorkspaceResolver {
private readonly userWorkspaceService: UserWorkspaceService,
private readonly workspaceInvitationService: WorkspaceInvitationService,
) {}
@Mutation(() => User)
async addUserToWorkspace(
@AuthUser() user: User,
@Args() workspaceInviteHashValidInput: WorkspaceInviteHashValidInput,
) {
const workspace = await this.workspaceRepository.findOneBy({
inviteHash: workspaceInviteHashValidInput.inviteHash,
});
if (!workspace) {
return;
}
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
workspace.id,
user.email,
);
return await this.userWorkspaceService.addUserToWorkspace(user, workspace);
}
@Mutation(() => User)
async addUserToWorkspaceByInviteToken(
@AuthUser() user: User,
@Args() workspaceInviteTokenInput: WorkspaceInviteTokenInput,
) {
return this.userWorkspaceService.addUserToWorkspaceByInviteToken(
workspaceInviteTokenInput.inviteToken,
user,
);
}
}

View File

@ -124,21 +124,6 @@ export class UserWorkspaceService extends TypeOrmQueryService<UserWorkspace> {
return user;
}
async addUserToWorkspaceByInviteToken(inviteToken: string, user: User) {
const appToken =
await this.workspaceInvitationService.validatePersonalInvitation({
workspacePersonalInviteToken: inviteToken,
email: user.email,
});
await this.workspaceInvitationService.invalidateWorkspaceInvitation(
appToken.workspace.id,
user.email,
);
return await this.addUserToWorkspace(user, appToken.workspace);
}
public async getUserCount(workspaceId: string): Promise<number | undefined> {
return await this.userWorkspaceRepository.countBy({
workspaceId,