refactor(auth): add workspaces selection (#12098)
This commit is contained in:
@ -3,6 +3,7 @@ import {
|
||||
GetCurrentUserDocument,
|
||||
GetLoginTokenFromCredentialsDocument,
|
||||
SignUpDocument,
|
||||
SignUpInWorkspaceDocument,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
export const queries = {
|
||||
@ -10,6 +11,7 @@ export const queries = {
|
||||
getAuthTokensFromLoginToken: GetAuthTokensFromLoginTokenDocument,
|
||||
signup: SignUpDocument,
|
||||
getCurrentUser: GetCurrentUserDocument,
|
||||
signUpInWorkspace: SignUpInWorkspaceDocument,
|
||||
};
|
||||
|
||||
export const email = 'test@test.com';
|
||||
@ -29,7 +31,13 @@ export const variables = {
|
||||
email,
|
||||
password,
|
||||
workspacePersonalInviteToken: null,
|
||||
locale: "",
|
||||
locale: '',
|
||||
},
|
||||
signUpInWorkspace: {
|
||||
email,
|
||||
password,
|
||||
workspacePersonalInviteToken: null,
|
||||
locale: '',
|
||||
},
|
||||
getCurrentUser: {},
|
||||
};
|
||||
@ -48,6 +56,16 @@ export const results = {
|
||||
},
|
||||
},
|
||||
signUp: { loginToken: { token, expiresAt: 'expiresAt' } },
|
||||
signUpInWorkspace: {
|
||||
loginToken: { token, expiresAt: 'expiresAt' },
|
||||
workspace: {
|
||||
id: 'workspace-id',
|
||||
workspaceUrls: {
|
||||
subdomainUrl: 'https://subdomain.twenty.com',
|
||||
customUrl: 'https://custom.twenty.com',
|
||||
},
|
||||
},
|
||||
},
|
||||
getCurrentUser: {
|
||||
currentUser: {
|
||||
id: 'id',
|
||||
@ -67,6 +85,7 @@ export const results = {
|
||||
avatarUrl: 'avatarUrl',
|
||||
locale: 'locale',
|
||||
},
|
||||
availableWorkspaces: [],
|
||||
currentWorkspace: {
|
||||
id: 'id',
|
||||
displayName: 'displayName',
|
||||
@ -74,6 +93,11 @@ export const results = {
|
||||
inviteHash: 'inviteHash',
|
||||
allowImpersonation: true,
|
||||
subscriptionStatus: 'subscriptionStatus',
|
||||
customDomain: null,
|
||||
workspaceUrls: {
|
||||
customUrl: undefined,
|
||||
subdomainUrl: 'https://twenty.com',
|
||||
},
|
||||
featureFlags: {
|
||||
id: 'id',
|
||||
key: 'key',
|
||||
@ -85,8 +109,8 @@ export const results = {
|
||||
},
|
||||
};
|
||||
|
||||
export const mocks = [
|
||||
{
|
||||
export const mocks = {
|
||||
getLoginTokenFromCredentials: {
|
||||
request: {
|
||||
query: queries.getLoginTokenFromCredentials,
|
||||
variables: variables.getLoginTokenFromCredentials,
|
||||
@ -97,7 +121,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
getAuthTokensFromLoginToken: {
|
||||
request: {
|
||||
query: queries.getAuthTokensFromLoginToken,
|
||||
variables: variables.getAuthTokensFromLoginToken,
|
||||
@ -108,7 +132,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
signup: {
|
||||
request: {
|
||||
query: queries.signup,
|
||||
variables: variables.signup,
|
||||
@ -119,7 +143,7 @@ export const mocks = [
|
||||
},
|
||||
})),
|
||||
},
|
||||
{
|
||||
getCurrentUser: {
|
||||
request: {
|
||||
query: queries.getCurrentUser,
|
||||
variables: variables.getCurrentUser,
|
||||
@ -128,4 +152,15 @@ export const mocks = [
|
||||
data: results.getCurrentUser,
|
||||
})),
|
||||
},
|
||||
];
|
||||
signUpInWorkspace: {
|
||||
request: {
|
||||
query: queries.signUpInWorkspace,
|
||||
variables: variables.signUpInWorkspace,
|
||||
},
|
||||
result: jest.fn(() => ({
|
||||
data: {
|
||||
signUpInWorkspace: results.signUpInWorkspace,
|
||||
},
|
||||
})),
|
||||
},
|
||||
};
|
||||
|
||||
@ -2,6 +2,7 @@ import { useAuth } from '@/auth/hooks/useAuth';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { SnackBarProviderScope } from '@/ui/feedback/snack-bar-manager/scopes/SnackBarProviderScope';
|
||||
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
|
||||
import { useApolloClient } from '@apollo/client';
|
||||
import { MockedProvider } from '@apollo/client/testing';
|
||||
@ -30,9 +31,13 @@ jest.mock('@/object-metadata/hooks/useRefreshObjectMetadataItem', () => ({
|
||||
}));
|
||||
|
||||
const Wrapper = ({ children }: { children: ReactNode }) => (
|
||||
<MockedProvider mocks={mocks} addTypename={false}>
|
||||
<MockedProvider mocks={Object.values(mocks)} addTypename={false}>
|
||||
<RecoilRoot>
|
||||
<MemoryRouter>{children}</MemoryRouter>
|
||||
<MemoryRouter>
|
||||
<SnackBarProviderScope snackBarManagerScopeId="test-scope-id">
|
||||
{children}
|
||||
</SnackBarProviderScope>
|
||||
</MemoryRouter>
|
||||
</RecoilRoot>
|
||||
</MockedProvider>
|
||||
);
|
||||
@ -63,7 +68,7 @@ describe('useAuth', () => {
|
||||
).toStrictEqual(results.getLoginTokenFromCredentials);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should verify user', async () => {
|
||||
@ -73,19 +78,19 @@ describe('useAuth', () => {
|
||||
await result.current.getAuthTokensFromLoginToken(token);
|
||||
});
|
||||
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks[3].result).toHaveBeenCalled();
|
||||
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
|
||||
expect(mocks.getCurrentUser.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle credential sign-in', async () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.signInWithCredentials(email, password);
|
||||
await result.current.signInWithCredentialsInWorkspace(email, password);
|
||||
});
|
||||
|
||||
expect(mocks[0].result).toHaveBeenCalled();
|
||||
expect(mocks[1].result).toHaveBeenCalled();
|
||||
expect(mocks.getLoginTokenFromCredentials.result).toHaveBeenCalled();
|
||||
expect(mocks.getAuthTokensFromLoginToken.result).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle google sign-in', async () => {
|
||||
@ -94,6 +99,7 @@ describe('useAuth', () => {
|
||||
await act(async () => {
|
||||
await result.current.signInWithGoogle({
|
||||
workspaceInviteHash: 'workspaceInviteHash',
|
||||
action: 'join-workspace',
|
||||
});
|
||||
});
|
||||
|
||||
@ -163,9 +169,12 @@ describe('useAuth', () => {
|
||||
const { result } = renderHooks();
|
||||
|
||||
await act(async () => {
|
||||
await result.current.signUpWithCredentials({ email, password });
|
||||
await result.current.signUpWithCredentialsInWorkspace({
|
||||
email,
|
||||
password,
|
||||
});
|
||||
});
|
||||
|
||||
expect(mocks[2].result).toHaveBeenCalled();
|
||||
expect(mocks.signUpInWorkspace.result).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
||||
@ -11,19 +11,21 @@ import {
|
||||
|
||||
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { workspacesState } from '@/auth/states/workspaces';
|
||||
import { billingState } from '@/client-config/states/billingState';
|
||||
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
|
||||
import { supportChatState } from '@/client-config/states/supportChatState';
|
||||
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
|
||||
import { REACT_APP_SERVER_BASE_URL } from '~/config';
|
||||
import {
|
||||
AuthTokenPair,
|
||||
useCheckUserExistsLazyQuery,
|
||||
useGetAuthTokensFromLoginTokenMutation,
|
||||
useGetCurrentUserLazyQuery,
|
||||
useGetLoginTokenFromCredentialsMutation,
|
||||
useGetLoginTokenFromEmailVerificationTokenMutation,
|
||||
useSignUpMutation,
|
||||
useSignInMutation,
|
||||
useSignUpInWorkspaceMutation,
|
||||
} from '~/generated/graphql';
|
||||
|
||||
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
|
||||
@ -68,10 +70,17 @@ import { iconsState } from 'twenty-ui/display';
|
||||
import { cookieStorage } from '~/utils/cookie-storage';
|
||||
import { getWorkspaceUrl } from '~/utils/getWorkspaceUrl';
|
||||
import { dynamicActivate } from '~/utils/i18n/dynamicActivate';
|
||||
import { availableWorkspacesState } from '@/auth/states/availableWorkspacesState';
|
||||
import { useSignUpInNewWorkspace } from '@/auth/sign-in-up/hooks/useSignUpInNewWorkspace';
|
||||
import {
|
||||
countAvailableWorkspaces,
|
||||
getFirstAvailableWorkspaces,
|
||||
} from '@/auth/utils/availableWorkspacesUtils';
|
||||
|
||||
export const useAuth = () => {
|
||||
const setTokenPair = useSetRecoilState(tokenPairState);
|
||||
const setCurrentUser = useSetRecoilState(currentUserState);
|
||||
const setAvailableWorkspaces = useSetRecoilState(availableWorkspacesState);
|
||||
const setCurrentWorkspaceMember = useSetRecoilState(
|
||||
currentWorkspaceMemberState,
|
||||
);
|
||||
@ -87,16 +96,18 @@ export const useAuth = () => {
|
||||
);
|
||||
|
||||
const { refreshObjectMetadataItems } = useRefreshObjectMetadataItems();
|
||||
const { createWorkspace } = useSignUpInNewWorkspace();
|
||||
|
||||
const setSignInUpStep = useSetRecoilState(signInUpStepState);
|
||||
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
|
||||
const setWorkspaces = useSetRecoilState(workspacesState);
|
||||
const { redirect } = useRedirect();
|
||||
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
|
||||
|
||||
const [getLoginTokenFromCredentials] =
|
||||
useGetLoginTokenFromCredentialsMutation();
|
||||
const [signIn] = useSignInMutation();
|
||||
const [signUp] = useSignUpMutation();
|
||||
const [signUpInWorkspace] = useSignUpInWorkspaceMutation();
|
||||
const [getAuthTokensFromLoginToken] =
|
||||
useGetAuthTokensFromLoginTokenMutation();
|
||||
const [getLoginTokenFromEmailVerificationToken] =
|
||||
@ -280,6 +291,10 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers(workspaceMembers);
|
||||
}
|
||||
|
||||
if (isDefined(user.availableWorkspaces)) {
|
||||
setAvailableWorkspaces(user.availableWorkspaces);
|
||||
}
|
||||
|
||||
if (isDefined(user.currentUserWorkspace)) {
|
||||
setCurrentUserWorkspace(user.currentUserWorkspace);
|
||||
}
|
||||
@ -326,17 +341,6 @@ export const useAuth = () => {
|
||||
});
|
||||
}
|
||||
|
||||
if (isDefined(user.workspaces)) {
|
||||
const validWorkspaces = user.workspaces
|
||||
.filter(
|
||||
({ workspace }) => workspace !== null && workspace !== undefined,
|
||||
)
|
||||
.map((validWorkspace) => validWorkspace.workspace)
|
||||
.filter(isDefined);
|
||||
|
||||
setWorkspaces(validWorkspaces);
|
||||
}
|
||||
|
||||
return {
|
||||
user,
|
||||
workspaceMember,
|
||||
@ -352,9 +356,17 @@ export const useAuth = () => {
|
||||
setCurrentWorkspaceMembers,
|
||||
setDateTimeFormat,
|
||||
setLastAuthenticateWorkspaceDomain,
|
||||
setWorkspaces,
|
||||
setAvailableWorkspaces,
|
||||
]);
|
||||
|
||||
const handleSetAuthTokens = useCallback(
|
||||
(tokens: AuthTokenPair) => {
|
||||
setTokenPair(tokens);
|
||||
cookieStorage.setItem('tokenPair', JSON.stringify(tokens));
|
||||
},
|
||||
[setTokenPair],
|
||||
);
|
||||
|
||||
const handleGetAuthTokensFromLoginToken = useCallback(
|
||||
async (loginToken: string) => {
|
||||
const getAuthTokensResult = await getAuthTokensFromLoginToken({
|
||||
@ -372,14 +384,8 @@ export const useAuth = () => {
|
||||
throw new Error('No getAuthTokensFromLoginToken result');
|
||||
}
|
||||
|
||||
setTokenPair(
|
||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||
);
|
||||
cookieStorage.setItem(
|
||||
'tokenPair',
|
||||
JSON.stringify(
|
||||
getAuthTokensResult.data?.getAuthTokensFromLoginToken.tokens,
|
||||
),
|
||||
handleSetAuthTokens(
|
||||
getAuthTokensResult.data.getAuthTokensFromLoginToken.tokens,
|
||||
);
|
||||
|
||||
// TODO: We can't parallelize this yet because when loadCurrentUSer is loaded
|
||||
@ -390,14 +396,97 @@ export const useAuth = () => {
|
||||
},
|
||||
[
|
||||
getAuthTokensFromLoginToken,
|
||||
setTokenPair,
|
||||
loadCurrentUser,
|
||||
origin,
|
||||
handleSetAuthTokens,
|
||||
refreshObjectMetadataItems,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignIn = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
signIn({
|
||||
variables: { email, password, captchaToken },
|
||||
onCompleted: async (data) => {
|
||||
handleSetAuthTokens(data.signIn.tokens);
|
||||
const { user } = await loadCurrentUser();
|
||||
|
||||
const availableWorkspacesCount = countAvailableWorkspaces(
|
||||
user.availableWorkspaces,
|
||||
);
|
||||
|
||||
if (availableWorkspacesCount === 0) {
|
||||
return createWorkspace();
|
||||
}
|
||||
|
||||
if (availableWorkspacesCount === 1) {
|
||||
const targetWorkspace = getFirstAvailableWorkspaces(
|
||||
user.availableWorkspaces,
|
||||
);
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(targetWorkspace.workspaceUrls),
|
||||
targetWorkspace.loginToken ? AppPath.Verify : AppPath.SignInUp,
|
||||
{
|
||||
...(targetWorkspace.loginToken && {
|
||||
loginToken: targetWorkspace.loginToken,
|
||||
}),
|
||||
email: user.email,
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
onError: (error) => {
|
||||
if (
|
||||
error instanceof ApolloError &&
|
||||
error.graphQLErrors[0]?.extensions?.subCode === 'EMAIL_NOT_VERIFIED'
|
||||
) {
|
||||
setSearchParams({ email });
|
||||
setSignInUpStep(SignInUpStep.EmailVerification);
|
||||
throw error;
|
||||
}
|
||||
throw error;
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
handleSetAuthTokens,
|
||||
redirectToWorkspaceDomain,
|
||||
signIn,
|
||||
loadCurrentUser,
|
||||
setSearchParams,
|
||||
setSignInUpStep,
|
||||
createWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
signUp({
|
||||
variables: { email, password, captchaToken },
|
||||
onCompleted: async (data) => {
|
||||
handleSetAuthTokens(data.signUp.tokens);
|
||||
const { user } = await loadCurrentUser();
|
||||
|
||||
if (countAvailableWorkspaces(user.availableWorkspaces) === 0) {
|
||||
return createWorkspace();
|
||||
}
|
||||
|
||||
setSignInUpStep(SignInUpStep.WorkspaceSelection);
|
||||
},
|
||||
});
|
||||
},
|
||||
[
|
||||
handleSetAuthTokens,
|
||||
signUp,
|
||||
loadCurrentUser,
|
||||
setSignInUpStep,
|
||||
createWorkspace,
|
||||
],
|
||||
);
|
||||
|
||||
const handleCredentialsSignInInWorkspace = useCallback(
|
||||
async (email: string, password: string, captchaToken?: string) => {
|
||||
const { loginToken } = await handleGetLoginTokenFromCredentials(
|
||||
email,
|
||||
@ -413,7 +502,7 @@ export const useAuth = () => {
|
||||
await clearSession();
|
||||
}, [clearSession]);
|
||||
|
||||
const handleCredentialsSignUp = useCallback(
|
||||
const handleCredentialsSignUpInWorkspace = useCallback(
|
||||
async ({
|
||||
email,
|
||||
password,
|
||||
@ -429,7 +518,7 @@ export const useAuth = () => {
|
||||
captchaToken?: string;
|
||||
verifyEmailNextPath?: string;
|
||||
}) => {
|
||||
const signUpResult = await signUp({
|
||||
const signUpInWorkspaceResult = await signUpInWorkspace({
|
||||
variables: {
|
||||
email,
|
||||
password,
|
||||
@ -444,11 +533,11 @@ export const useAuth = () => {
|
||||
},
|
||||
});
|
||||
|
||||
if (isDefined(signUpResult.errors)) {
|
||||
throw signUpResult.errors;
|
||||
if (isDefined(signUpInWorkspaceResult.errors)) {
|
||||
throw signUpInWorkspaceResult.errors;
|
||||
}
|
||||
|
||||
if (!signUpResult.data?.signUp) {
|
||||
if (!signUpInWorkspaceResult.data?.signUpInWorkspace) {
|
||||
throw new Error('No login token');
|
||||
}
|
||||
|
||||
@ -460,11 +549,15 @@ export const useAuth = () => {
|
||||
|
||||
if (isMultiWorkspaceEnabled) {
|
||||
return await redirectToWorkspaceDomain(
|
||||
getWorkspaceUrl(signUpResult.data.signUp.workspace.workspaceUrls),
|
||||
getWorkspaceUrl(
|
||||
signUpInWorkspaceResult.data.signUpInWorkspace.workspace
|
||||
.workspaceUrls,
|
||||
),
|
||||
isEmailVerificationRequired ? AppPath.SignInUp : AppPath.Verify,
|
||||
{
|
||||
...(!isEmailVerificationRequired && {
|
||||
loginToken: signUpResult.data.signUp.loginToken.token,
|
||||
loginToken:
|
||||
signUpInWorkspaceResult.data.signUpInWorkspace.loginToken.token,
|
||||
}),
|
||||
email,
|
||||
},
|
||||
@ -472,11 +565,11 @@ export const useAuth = () => {
|
||||
}
|
||||
|
||||
await handleGetAuthTokensFromLoginToken(
|
||||
signUpResult.data?.signUp.loginToken.token,
|
||||
signUpInWorkspaceResult.data?.signUpInWorkspace.loginToken.token,
|
||||
);
|
||||
},
|
||||
[
|
||||
signUp,
|
||||
signUpInWorkspace,
|
||||
workspacePublicData,
|
||||
isMultiWorkspaceEnabled,
|
||||
handleGetAuthTokensFromLoginToken,
|
||||
@ -494,6 +587,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action?: string;
|
||||
},
|
||||
) => {
|
||||
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
|
||||
@ -513,6 +607,10 @@ export const useAuth = () => {
|
||||
);
|
||||
}
|
||||
|
||||
if (isDefined(params.action)) {
|
||||
url.searchParams.set('action', params.action);
|
||||
}
|
||||
|
||||
if (isDefined(workspacePublicData)) {
|
||||
url.searchParams.set('workspaceId', workspacePublicData.id);
|
||||
}
|
||||
@ -527,6 +625,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action: string;
|
||||
}) => {
|
||||
redirect(buildRedirectUrl('/auth/google', params));
|
||||
},
|
||||
@ -538,6 +637,7 @@ export const useAuth = () => {
|
||||
workspacePersonalInviteToken?: string;
|
||||
workspaceInviteHash?: string;
|
||||
billingCheckoutSession?: BillingCheckoutSession;
|
||||
action: string;
|
||||
}) => {
|
||||
redirect(buildRedirectUrl('/auth/microsoft', params));
|
||||
},
|
||||
@ -556,8 +656,11 @@ export const useAuth = () => {
|
||||
clearSession,
|
||||
signOut: handleSignOut,
|
||||
signUpWithCredentials: handleCredentialsSignUp,
|
||||
signUpWithCredentialsInWorkspace: handleCredentialsSignUpInWorkspace,
|
||||
signInWithCredentialsInWorkspace: handleCredentialsSignInInWorkspace,
|
||||
signInWithCredentials: handleCredentialsSignIn,
|
||||
signInWithGoogle: handleGoogleLogin,
|
||||
signInWithMicrosoft: handleMicrosoftLogin,
|
||||
setAuthTokens: handleSetAuthTokens,
|
||||
};
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user