diff --git a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts index 84a9796eb2..510f3f66c7 100644 --- a/packages/twenty-front/src/modules/auth/hooks/useAuth.ts +++ b/packages/twenty-front/src/modules/auth/hooks/useAuth.ts @@ -48,6 +48,7 @@ import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/h import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; +import { useRedirect } from '@/domain-manager/hooks/useRedirect'; export const useAuth = () => { const setTokenPair = useSetRecoilState(tokenPairState); @@ -65,6 +66,7 @@ export const useAuth = () => { const setCurrentWorkspace = useSetRecoilState(currentWorkspaceState); const setIsVerifyPendingState = useSetRecoilState(isVerifyPendingState); const setWorkspaces = useSetRecoilState(workspacesState); + const { redirect } = useRedirect(); const [challenge] = useChallengeMutation(); const [signUp] = useSignUpMutation(); @@ -367,9 +369,9 @@ export const useAuth = () => { workspacePersonalInviteToken?: string; workspaceInviteHash?: string; }) => { - window.location.href = buildRedirectUrl('/auth/google', params); + redirect(buildRedirectUrl('/auth/google', params)); }, - [buildRedirectUrl], + [buildRedirectUrl, redirect], ); const handleMicrosoftLogin = useCallback( @@ -377,9 +379,9 @@ export const useAuth = () => { workspacePersonalInviteToken?: string; workspaceInviteHash?: string; }) => { - window.location.href = buildRedirectUrl('/auth/microsoft', params); + redirect(buildRedirectUrl('/auth/microsoft', params)); }, - [buildRedirectUrl], + [buildRedirectUrl, redirect], ); return { diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts index e61ff3730d..eff2046f2b 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useGetPublicWorkspaceDataBySubdomain.ts @@ -1,7 +1,6 @@ import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState'; import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { useIsCurrentLocationOnDefaultDomain } from '@/domain-manager/hooks/useIsCurrentLocationOnDefaultDomain'; -import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain'; import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState'; import { useRecoilValue, useSetRecoilState } from 'recoil'; @@ -19,8 +18,6 @@ export const useGetPublicWorkspaceDataBySubdomain = () => { const setWorkspacePublicDataState = useSetRecoilState( workspacePublicDataState, ); - const { setLastAuthenticateWorkspaceDomain } = - useLastAuthenticatedWorkspaceDomain(); const { loading } = useGetPublicWorkspaceDataBySubdomainQuery({ skip: @@ -35,7 +32,6 @@ export const useGetPublicWorkspaceDataBySubdomain = () => { onError: (error) => { // eslint-disable-next-line no-console console.error(error); - setLastAuthenticateWorkspaceDomain(null); redirectToDefaultDomain(); }, }); diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirect.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirect.ts new file mode 100644 index 0000000000..241c034fdd --- /dev/null +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirect.ts @@ -0,0 +1,14 @@ +// Don't use this hook directly! Prefer the high level hooks like: +// useRedirectToDefaultDomain and useRedirectToWorkspaceDomain + +import { useDebouncedCallback } from 'use-debounce'; + +export const useRedirect = () => { + const redirect = useDebouncedCallback((url: string) => { + window.location.href = url; + }, 1); + + return { + redirect, + }; +}; diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts index e5070db28f..6fb863033b 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToDefaultDomain.ts @@ -1,12 +1,19 @@ import { useReadDefaultDomainFromConfiguration } from '@/domain-manager/hooks/useReadDefaultDomainFromConfiguration'; +import { useRedirect } from '@/domain-manager/hooks/useRedirect'; +import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain'; export const useRedirectToDefaultDomain = () => { const { defaultDomain } = useReadDefaultDomainFromConfiguration(); + const { setLastAuthenticateWorkspaceDomain } = + useLastAuthenticatedWorkspaceDomain(); + + const { redirect } = useRedirect(); const redirectToDefaultDomain = () => { const url = new URL(window.location.href); if (url.hostname !== defaultDomain) { + setLastAuthenticateWorkspaceDomain(null); url.hostname = defaultDomain; - window.location.href = url.toString(); + redirect(url.toString()); } }; diff --git a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts index 2d95e809d7..091dd8d3d7 100644 --- a/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts +++ b/packages/twenty-front/src/modules/domain-manager/hooks/useRedirectToWorkspaceDomain.ts @@ -1,10 +1,12 @@ import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState'; import { useRecoilValue } from 'recoil'; import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; +import { useRedirect } from '@/domain-manager/hooks/useRedirect'; export const useRedirectToWorkspaceDomain = () => { const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState); const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); + const { redirect } = useRedirect(); const redirectToWorkspaceDomain = ( subdomain: string, @@ -12,7 +14,7 @@ export const useRedirectToWorkspaceDomain = () => { searchParams?: Record, ) => { if (!isMultiWorkspaceEnabled) return; - window.location.href = buildWorkspaceUrl(subdomain, pathname, searchParams); + redirect(buildWorkspaceUrl(subdomain, pathname, searchParams)); }; return { diff --git a/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerApiOAuth.ts b/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerApiOAuth.ts index f66f6ca6de..cdd8046e51 100644 --- a/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerApiOAuth.ts +++ b/packages/twenty-front/src/modules/settings/accounts/hooks/useTriggerApiOAuth.ts @@ -7,6 +7,7 @@ import { MessageChannelVisibility, useGenerateTransientTokenMutation, } from '~/generated/graphql'; +import { useRedirect } from '@/domain-manager/hooks/useRedirect'; const getProviderUrl = (provider: string) => { switch (provider) { @@ -21,6 +22,7 @@ const getProviderUrl = (provider: string) => { export const useTriggerApisOAuth = () => { const [generateTransientToken] = useGenerateTransientTokenMutation(); + const { redirect } = useRedirect(); const triggerApisOAuth = useCallback( async ( @@ -60,9 +62,9 @@ export const useTriggerApisOAuth = () => { params += loginHint ? `&loginHint=${loginHint}` : ''; - window.location.href = `${authServerUrl}/auth/${getProviderUrl(provider)}?${params}`; + redirect(`${authServerUrl}/auth/${getProviderUrl(provider)}?${params}`); }, - [generateTransientToken], + [generateTransientToken, redirect], ); return { triggerApisOAuth }; diff --git a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts index 1046c70c91..a0b18d235d 100644 --- a/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts +++ b/packages/twenty-front/src/modules/settings/admin-panel/hooks/useImpersonate.ts @@ -6,10 +6,11 @@ import { useState } from 'react'; import { useRecoilState, useSetRecoilState } from 'recoil'; import { useImpersonateMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; -import { sleep } from '~/utils/sleep'; +import { useRedirect } from '@/domain-manager/hooks/useRedirect'; export const useImpersonate = () => { const { clearSession } = useAuth(); + const { redirect } = useRedirect(); const [currentUser, setCurrentUser] = useRecoilState(currentUserState); const setTokenPair = useSetRecoilState(tokenPairState); const [impersonate] = useImpersonateMutation(); @@ -43,8 +44,7 @@ export const useImpersonate = () => { await clearSession(); setCurrentUser(user); setTokenPair(tokens); - await sleep(0); // This hacky workaround is necessary to ensure the tokens stored in the cookie are updated correctly. - window.location.href = AppPath.Index; + redirect(AppPath.Index); } catch (error) { setError('Failed to impersonate user. Please try again.'); setIsLoading(false); diff --git a/packages/twenty-front/src/pages/auth/Authorize.tsx b/packages/twenty-front/src/pages/auth/Authorize.tsx index f2745a7547..407b609c16 100644 --- a/packages/twenty-front/src/pages/auth/Authorize.tsx +++ b/packages/twenty-front/src/pages/auth/Authorize.tsx @@ -6,6 +6,7 @@ import { useNavigate, useSearchParams } from 'react-router-dom'; import { MainButton, UndecoratedLink } from 'twenty-ui'; import { useAuthorizeAppMutation } from '~/generated/graphql'; import { isDefined } from '~/utils/isDefined'; +import { useRedirect } from '@/domain-manager/hooks/useRedirect'; type App = { id: string; name: string; logo: string }; @@ -55,6 +56,7 @@ const StyledButtonContainer = styled.div` export const Authorize = () => { const navigate = useNavigate(); const [searchParam] = useSearchParams(); + const { redirect } = useRedirect(); //TODO: Replace with db call for registered third party apps const [apps] = useState([ { @@ -89,7 +91,7 @@ export const Authorize = () => { redirectUrl, }, onCompleted: (data) => { - window.location.href = data.authorizeApp.redirectUrl; + redirect(data.authorizeApp.redirectUrl); }, }); } diff --git a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx index 2febb79f8a..256daeadaf 100644 --- a/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx +++ b/packages/twenty-front/src/pages/settings/workspace/SettingsDomain.tsx @@ -17,7 +17,7 @@ import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar'; import { useUpdateWorkspaceMutation } from '~/generated/graphql'; import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState'; import { isDefined } from '~/utils/isDefined'; -import { useBuildWorkspaceUrl } from '@/domain-manager/hooks/useBuildWorkspaceUrl'; +import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain'; const validationSchema = z .object({ @@ -54,7 +54,7 @@ export const SettingsDomain = () => { const { enqueueSnackBar } = useSnackBar(); const [updateWorkspace] = useUpdateWorkspaceMutation(); - const { buildWorkspaceUrl } = useBuildWorkspaceUrl(); + const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain(); const [currentWorkspace, setCurrentWorkspace] = useRecoilState( currentWorkspaceState, @@ -97,7 +97,7 @@ export const SettingsDomain = () => { subdomain: values.subdomain, }); - window.location.href = buildWorkspaceUrl(values.subdomain); + redirectToWorkspaceDomain(values.subdomain); } catch (error) { if ( error instanceof Error && diff --git a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts index cc6e90287d..6e43cacd7f 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/services/sso.service.ts @@ -25,7 +25,6 @@ import { OIDCResponseType, WorkspaceSSOIdentityProvider, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; -import { User } from 'src/engine/core-modules/user/user.entity'; @Injectable() export class SSOService { @@ -35,8 +34,6 @@ export class SSOService { private readonly featureFlagRepository: Repository, @InjectRepository(WorkspaceSSOIdentityProvider, 'core') private readonly workspaceSSOIdentityProviderRepository: Repository, - @InjectRepository(User, 'core') - private readonly userRepository: Repository, private readonly environmentService: EnvironmentService, private readonly billingService: BillingService, ) {} diff --git a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx index 52dce83f38..1c63ee63da 100644 --- a/packages/twenty-website/src/content/developers/self-hosting/setup.mdx +++ b/packages/twenty-website/src/content/developers/self-hosting/setup.mdx @@ -116,6 +116,7 @@ yarn command:prod cron:calendar:ongoing-stale ['AUTH_GOOGLE_CLIENT_SECRET', '', 'Google client secret'], ['AUTH_GOOGLE_CALLBACK_URL', 'https://[YourDomain]/auth/google/redirect', 'Google auth callback'], ['AUTH_MICROSOFT_ENABLED', 'false', 'Enable Microsoft SSO login'], + ['AUTH_SSO_ENABLED', 'false', 'Enable SSO with SAML or OIDC'], ['AUTH_MICROSOFT_CLIENT_ID', '', 'Microsoft client ID'], ['AUTH_MICROSOFT_TENANT_ID', '', 'Microsoft tenant ID'], ['AUTH_MICROSOFT_CLIENT_SECRET', '', 'Microsoft client secret'],