Files
twenty/packages/twenty-front/src/modules/auth/hooks/useAuth.ts

387 lines
13 KiB
TypeScript

import { useApolloClient } from '@apollo/client';
import { useCallback } from 'react';
import {
snapshot_UNSTABLE,
useGotoRecoilSnapshot,
useRecoilCallback,
useSetRecoilState,
} from 'recoil';
import { iconsState } from 'twenty-ui';
import { currentWorkspaceMemberState } from '@/auth/states/currentWorkspaceMemberState';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { isCurrentUserLoadedState } from '@/auth/states/isCurrentUserLoadingState';
import { isVerifyPendingState } from '@/auth/states/isVerifyPendingState';
import { workspacesState } from '@/auth/states/workspaces';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { clientConfigApiStatusState } from '@/client-config/states/clientConfigApiStatusState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { ColorScheme } from '@/workspace-member/types/WorkspaceMember';
import { REACT_APP_SERVER_BASE_URL } from '~/config';
import {
useChallengeMutation,
useCheckUserExistsLazyQuery,
useSignUpMutation,
useVerifyMutation,
} from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { currentWorkspaceMembersState } from '@/auth/states/currentWorkspaceMembersStates';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { DateFormat } from '@/localization/constants/DateFormat';
import { TimeFormat } from '@/localization/constants/TimeFormat';
import { dateTimeFormatState } from '@/localization/states/dateTimeFormatState';
import { detectDateFormat } from '@/localization/utils/detectDateFormat';
import { detectTimeFormat } from '@/localization/utils/detectTimeFormat';
import { detectTimeZone } from '@/localization/utils/detectTimeZone';
import { getDateFormatFromWorkspaceDateFormat } from '@/localization/utils/getDateFormatFromWorkspaceDateFormat';
import { getTimeFormatFromWorkspaceTimeFormat } from '@/localization/utils/getTimeFormatFromWorkspaceTimeFormat';
import { currentUserState } from '../states/currentUserState';
import { tokenPairState } from '../states/tokenPairState';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { useIsCurrentLocationOnAWorkspaceSubdomain } from '@/domain-manager/hooks/useIsCurrentLocationOnAWorkspaceSubdomain';
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
const setCurrentUser = useSetRecoilState(currentUserState);
const setCurrentWorkspaceMember = useSetRecoilState(
currentWorkspaceMemberState,
);
const setCurrentWorkspaceMembers = useSetRecoilState(
currentWorkspaceMembersState,
);
const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState);
const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState);
const setWorkspaces = useSetRecoilState(workspacesState);
const [challenge] = useChallengeMutation();
const [signUp] = useSignUpMutation();
const [verify] = useVerifyMutation();
const { isOnAWorkspaceSubdomain } =
useIsCurrentLocationOnAWorkspaceSubdomain();
const { workspaceSubdomain } = useReadWorkspaceSubdomainFromCurrentLocation();
const { setLastAuthenticateWorkspaceDomain } =
useLastAuthenticatedWorkspaceDomain();
const [checkUserExistsQuery, { data: checkUserExistsData }] =
useCheckUserExistsLazyQuery();
const client = useApolloClient();
const goToRecoilSnapshot = useGotoRecoilSnapshot();
const setDateTimeFormat = useSetRecoilState(dateTimeFormatState);
const clearSession = useRecoilCallback(
({ snapshot }) =>
async () => {
const emptySnapshot = snapshot_UNSTABLE();
const iconsValue = snapshot.getLoadable(iconsState).getValue();
const authProvidersValue = snapshot
.getLoadable(authProvidersState)
.getValue();
const billing = snapshot.getLoadable(billingState).getValue();
const isDeveloperDefaultSignInPrefilled = snapshot
.getLoadable(isDeveloperDefaultSignInPrefilledState)
.getValue();
const supportChat = snapshot.getLoadable(supportChatState).getValue();
const isDebugMode = snapshot.getLoadable(isDebugModeState).getValue();
const captchaProvider = snapshot
.getLoadable(captchaProviderState)
.getValue();
const clientConfigApiStatus = snapshot
.getLoadable(clientConfigApiStatusState)
.getValue();
const isCurrentUserLoaded = snapshot
.getLoadable(isCurrentUserLoadedState)
.getValue();
const isMultiWorkspaceEnabled = snapshot
.getLoadable(isMultiWorkspaceEnabledState)
.getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(iconsState, iconsValue);
set(authProvidersState, authProvidersValue);
set(billingState, billing);
set(
isDeveloperDefaultSignInPrefilledState,
isDeveloperDefaultSignInPrefilled,
);
set(supportChatState, supportChat);
set(isDebugModeState, isDebugMode);
set(captchaProviderState, captchaProvider);
set(clientConfigApiStatusState, clientConfigApiStatus);
set(isCurrentUserLoadedState, isCurrentUserLoaded);
set(isMultiWorkspaceEnabledState, isMultiWorkspaceEnabled);
return undefined;
});
goToRecoilSnapshot(initialSnapshot);
await client.clearStore();
sessionStorage.clear();
localStorage.clear();
},
[client, goToRecoilSnapshot],
);
const handleChallenge = useCallback(
async (email: string, password: string, captchaToken?: string) => {
const challengeResult = await challenge({
variables: {
email,
password,
captchaToken,
},
});
if (isDefined(challengeResult.errors)) {
throw challengeResult.errors;
}
if (!challengeResult.data?.challenge) {
throw new Error('No login token');
}
return challengeResult.data.challenge;
},
[challenge],
);
const handleVerify = useCallback(
async (loginToken: string) => {
const verifyResult = await verify({
variables: { loginToken },
});
if (isDefined(verifyResult.errors)) {
throw verifyResult.errors;
}
if (!verifyResult.data?.verify) {
throw new Error('No verify result');
}
setTokenPair(verifyResult.data?.verify.tokens);
const user = verifyResult.data?.verify.user;
let workspaceMember = null;
setCurrentUser(user);
if (isDefined(user.workspaceMembers)) {
const workspaceMembers = user.workspaceMembers.map(
(workspaceMember) => ({
...workspaceMember,
colorScheme: workspaceMember.colorScheme as ColorScheme,
locale: workspaceMember.locale ?? 'en',
}),
);
setCurrentWorkspaceMembers(workspaceMembers);
}
if (isDefined(user.workspaceMember)) {
workspaceMember = {
...user.workspaceMember,
colorScheme: user.workspaceMember?.colorScheme as ColorScheme,
locale: user.workspaceMember?.locale ?? 'en',
};
setCurrentWorkspaceMember(workspaceMember);
// TODO: factorize with UserProviderEffect
setDateTimeFormat({
timeZone:
workspaceMember.timeZone && workspaceMember.timeZone !== 'system'
? workspaceMember.timeZone
: detectTimeZone(),
dateFormat: isDefined(user.workspaceMember.dateFormat)
? getDateFormatFromWorkspaceDateFormat(
user.workspaceMember.dateFormat,
)
: DateFormat[detectDateFormat()],
timeFormat: isDefined(user.workspaceMember.timeFormat)
? getTimeFormatFromWorkspaceTimeFormat(
user.workspaceMember.timeFormat,
)
: TimeFormat[detectTimeFormat()],
});
}
const workspace = user.defaultWorkspace ?? null;
setCurrentWorkspace(workspace);
if (isDefined(workspace) && isOnAWorkspaceSubdomain) {
setLastAuthenticateWorkspaceDomain({
workspaceId: workspace.id,
subdomain: workspace.subdomain,
});
}
if (isDefined(verifyResult.data?.verify.user.workspaces)) {
const validWorkspaces = verifyResult.data?.verify.user.workspaces
.filter(
({ workspace }) => workspace !== null && workspace !== undefined,
)
.map((validWorkspace) => validWorkspace.workspace)
.filter(isDefined);
setWorkspaces(validWorkspaces);
}
return {
user,
workspaceMember,
workspace,
tokens: verifyResult.data?.verify.tokens,
};
},
[
verify,
setTokenPair,
setCurrentUser,
setCurrentWorkspace,
isOnAWorkspaceSubdomain,
setCurrentWorkspaceMembers,
setCurrentWorkspaceMember,
setDateTimeFormat,
setLastAuthenticateWorkspaceDomain,
setWorkspaces,
],
);
const handleCrendentialsSignIn = useCallback(
async (email: string, password: string, captchaToken?: string) => {
const { loginToken } = await handleChallenge(
email,
password,
captchaToken,
);
setIsVerifyPendingState(true);
const { user, workspaceMember, workspace } = await handleVerify(
loginToken.token,
);
setIsVerifyPendingState(false);
return {
user,
workspaceMember,
workspace,
};
},
[handleChallenge, handleVerify, setIsVerifyPendingState],
);
const handleSignOut = useCallback(async () => {
await clearSession();
}, [clearSession]);
const handleCredentialsSignUp = useCallback(
async (
email: string,
password: string,
workspaceInviteHash?: string,
workspacePersonalInviteToken?: string,
captchaToken?: string,
) => {
setIsVerifyPendingState(true);
const signUpResult = await signUp({
variables: {
email,
password,
workspaceInviteHash,
workspacePersonalInviteToken,
captchaToken,
},
});
if (isDefined(signUpResult.errors)) {
throw signUpResult.errors;
}
if (!signUpResult.data?.signUp) {
throw new Error('No login token');
}
const { user, workspace, workspaceMember } = await handleVerify(
signUpResult.data?.signUp.loginToken.token,
);
setIsVerifyPendingState(false);
return { user, workspaceMember, workspace };
},
[setIsVerifyPendingState, signUp, handleVerify],
);
const buildRedirectUrl = useCallback(
(
path: string,
params: {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
},
) => {
const url = new URL(`${REACT_APP_SERVER_BASE_URL}${path}`);
if (isDefined(params.workspaceInviteHash)) {
url.searchParams.set('inviteHash', params.workspaceInviteHash);
}
if (isDefined(params.workspacePersonalInviteToken)) {
url.searchParams.set(
'inviteToken',
params.workspacePersonalInviteToken,
);
}
if (isDefined(workspaceSubdomain)) {
url.searchParams.set('workspaceSubdomain', workspaceSubdomain);
}
return url.toString();
},
[workspaceSubdomain],
);
const handleGoogleLogin = useCallback(
(params: {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
}) => {
window.location.href = buildRedirectUrl('/auth/google', params);
},
[buildRedirectUrl],
);
const handleMicrosoftLogin = useCallback(
(params: {
workspacePersonalInviteToken?: string;
workspaceInviteHash?: string;
}) => {
window.location.href = buildRedirectUrl('/auth/microsoft', params);
},
[buildRedirectUrl],
);
return {
challenge: handleChallenge,
verify: handleVerify,
checkUserExists: { checkUserExistsData, checkUserExistsQuery },
clearSession,
signOut: handleSignOut,
signUpWithCredentials: handleCredentialsSignUp,
signInWithCredentials: handleCrendentialsSignIn,
signInWithGoogle: handleGoogleLogin,
signInWithMicrosoft: handleMicrosoftLogin,
};
};