Enforce system wide sso providers (#9058)

We have recently introduced the possibility to specify workspace
specific auth providers.
I'm:
- introducing system wide auth providers (provided by clientConfig)
- making sure workspace specific auth providers belong to system wide
auth providers set
This commit is contained in:
Charles Bochet 2024-12-13 16:38:04 +01:00 committed by GitHub
parent 57869d3c8c
commit 7e67b1c5a6
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
26 changed files with 382 additions and 236 deletions

View File

@ -1,5 +1,5 @@
import * as Apollo from '@apollo/client';
import { gql } from '@apollo/client';
import * as Apollo from '@apollo/client';
export type Maybe<T> = T | null;
export type InputMaybe<T> = Maybe<T>;
export type Exact<T extends { [key: string]: unknown }> = { [K in keyof T]: T[K] };
@ -169,6 +169,7 @@ export type ClientConfig = {
__typename?: 'ClientConfig';
analyticsEnabled: Scalars['Boolean'];
api: ApiConfig;
authProviders: AuthProviders;
billing: Billing;
captcha: Captcha;
chromeExtensionId?: Maybe<Scalars['String']>;
@ -1577,7 +1578,7 @@ export type Field = {
id: Scalars['UUID'];
isActive?: Maybe<Scalars['Boolean']>;
isCustom?: Maybe<Scalars['Boolean']>;
isLabelSyncedWithName: Scalars['Boolean'];
isLabelSyncedWithName?: Maybe<Scalars['Boolean']>;
isNullable?: Maybe<Scalars['Boolean']>;
isSystem?: Maybe<Scalars['Boolean']>;
isUnique?: Maybe<Scalars['Boolean']>;
@ -1970,7 +1971,7 @@ export type UpdateBillingSubscriptionMutation = { __typename?: 'Mutation', updat
export type GetClientConfigQueryVariables = Exact<{ [key: string]: never; }>;
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
export type GetClientConfigQuery = { __typename?: 'Query', clientConfig: { __typename?: 'ClientConfig', signInPrefilled: boolean, isMultiWorkspaceEnabled: boolean, isSSOEnabled: boolean, defaultSubdomain?: string | null, frontDomain: string, debugMode: boolean, analyticsEnabled: boolean, chromeExtensionId?: string | null, billing: { __typename?: 'Billing', isBillingEnabled: boolean, billingUrl?: string | null, billingFreeTrialDurationInDays?: number | null }, authProviders: { __typename?: 'AuthProviders', google: boolean, password: boolean, microsoft: boolean, sso: Array<{ __typename?: 'SSOIdentityProvider', id: string, name: string, type: IdentityProviderType, status: SsoIdentityProviderStatus, issuer: string }> }, support: { __typename?: 'Support', supportDriver: string, supportFrontChatId?: string | null }, sentry: { __typename?: 'Sentry', dsn?: string | null, environment?: string | null, release?: string | null }, captcha: { __typename?: 'Captcha', provider?: CaptchaDriverType | null, siteKey?: string | null }, api: { __typename?: 'ApiConfig', mutationMaximumAffectedRecords: number } } };
export type SkipSyncEmailOnboardingStepMutationVariables = Exact<{ [key: string]: never; }>;
@ -3355,6 +3356,18 @@ export const GetClientConfigDocument = gql`
billingUrl
billingFreeTrialDurationInDays
}
authProviders {
google
password
microsoft
sso {
id
name
type
status
issuer
}
}
signInPrefilled
isMultiWorkspaceEnabled
isSSOEnabled

View File

@ -7,14 +7,14 @@ import { RecoilRoot, useRecoilValue } from 'recoil';
import { iconsState } from 'twenty-ui';
import { useAuth } from '@/auth/hooks/useAuth';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { email, mocks, password, results, token } from '../__mocks__/useAuth';
const Wrapper = ({ children }: { children: ReactNode }) => (
<MockedProvider mocks={mocks} addTypename={false}>
@ -77,7 +77,9 @@ describe('useAuth', () => {
() => {
const client = useApolloClient();
const icons = useRecoilValue(iconsState);
const authProviders = useRecoilValue(authProvidersState);
const workspaceAuthProviders = useRecoilValue(
workspaceAuthProvidersState,
);
const billing = useRecoilValue(billingState);
const isDeveloperDefaultSignInPrefilled = useRecoilValue(
isDeveloperDefaultSignInPrefilledState,
@ -92,7 +94,7 @@ describe('useAuth', () => {
client,
state: {
icons,
authProviders,
workspaceAuthProviders,
billing,
isDeveloperDefaultSignInPrefilled,
supportChat,
@ -118,7 +120,7 @@ describe('useAuth', () => {
const { state } = result.current;
expect(state.icons).toEqual({});
expect(state.authProviders).toEqual({
expect(state.workspaceAuthProviders).toEqual({
google: true,
microsoft: false,
magicLink: false,

View File

@ -13,7 +13,6 @@ 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';
@ -48,6 +47,7 @@ import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useL
import { useReadWorkspaceSubdomainFromCurrentLocation } from '@/domain-manager/hooks/useReadWorkspaceSubdomainFromCurrentLocation';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isAppWaitingForFreshObjectMetadataState } from '@/object-metadata/states/isAppWaitingForFreshObjectMetadataState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
export const useAuth = () => {
const setTokenPair = useSetRecoilState(tokenPairState);
@ -90,7 +90,7 @@ export const useAuth = () => {
const emptySnapshot = snapshot_UNSTABLE();
const iconsValue = snapshot.getLoadable(iconsState).getValue();
const authProvidersValue = snapshot
.getLoadable(authProvidersState)
.getLoadable(workspaceAuthProvidersState)
.getValue();
const billing = snapshot.getLoadable(billingState).getValue();
const isDeveloperDefaultSignInPrefilled = snapshot
@ -115,7 +115,7 @@ export const useAuth = () => {
.getValue();
const initialSnapshot = emptySnapshot.map(({ set }) => {
set(iconsState, iconsValue);
set(authProvidersState, authProvidersValue);
set(workspaceAuthProvidersState, authProvidersValue);
set(billingState, billing);
set(
isDeveloperDefaultSignInPrefilledState,

View File

@ -1,37 +1,31 @@
import styled from '@emotion/styled';
import {
IconGoogle,
IconMicrosoft,
Loader,
MainButton,
HorizontalSeparator,
} from 'twenty-ui';
import { useTheme } from '@emotion/react';
import { useSignInWithGoogle } from '@/auth/sign-in-up/hooks/useSignInWithGoogle';
import { useSignInWithMicrosoft } from '@/auth/sign-in-up/hooks/useSignInWithMicrosoft';
import { FormProvider } from 'react-hook-form';
import { motion } from 'framer-motion';
import { useState } from 'react';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { FormProvider } from 'react-hook-form';
import { useLocation } from 'react-router-dom';
import { useRecoilState, useRecoilValue, useSetRecoilState } from 'recoil';
import { HorizontalSeparator, Loader, MainButton } from 'twenty-ui';
import { isDefined } from '~/utils/isDefined';
import { useAuth } from '@/auth/hooks/useAuth';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpEmailField } from '@/auth/sign-in-up/components/SignInUpEmailField';
import { SignInUpPasswordField } from '@/auth/sign-in-up/components/SignInUpPasswordField';
import { useAuth } from '@/auth/hooks/useAuth';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { signInUpModeState } from '@/auth/states/signInUpModeState';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { SignInUpMode } from '@/auth/types/signInUpMode';
import { useReadCaptchaToken } from '@/captcha/hooks/useReadCaptchaToken';
import { useRequestFreshCaptchaToken } from '@/captcha/hooks/useRequestFreshCaptchaToken';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { useRedirectToWorkspaceDomain } from '@/domain-manager/hooks/useRedirectToWorkspaceDomain';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { isDefined } from '~/utils/isDefined';
const StyledContentContainer = styled(motion.div)`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -46,11 +40,9 @@ const StyledForm = styled.form`
`;
export const SignInUpGlobalScopeForm = () => {
const theme = useTheme();
const authProviders = useRecoilValue(authProvidersState);
const signInUpStep = useRecoilValue(signInUpStepState);
const { signInWithGoogle } = useSignInWithGoogle();
const { signInWithMicrosoft } = useSignInWithMicrosoft();
const { checkUserExists } = useAuth();
const { readCaptchaToken } = useReadCaptchaToken();
const { redirectToWorkspaceDomain } = useRedirectToWorkspaceDomain();
@ -116,20 +108,9 @@ export const SignInUpGlobalScopeForm = () => {
return (
<>
<StyledContentContainer>
<MainButton
Icon={() => <IconGoogle size={theme.icon.size.lg} />}
title="Continue with Google"
onClick={signInWithGoogle}
fullWidth
/>
<HorizontalSeparator visible={false} />
<MainButton
Icon={() => <IconMicrosoft size={theme.icon.size.lg} />}
title="Continue with Microsoft"
onClick={signInWithMicrosoft}
fullWidth
/>
<HorizontalSeparator visible={false} />
{authProviders.google && <SignInUpWithGoogle />}
{authProviders.microsoft && <SignInUpWithMicrosoft />}
<HorizontalSeparator visible />
{/* eslint-disable-next-line react/jsx-props-no-spreading */}
<FormProvider {...form}>

View File

@ -4,10 +4,10 @@ import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
import styled from '@emotion/styled';
import { useRecoilValue } from 'recoil';
import { MainButton, HorizontalSeparator } from 'twenty-ui';
import { HorizontalSeparator, MainButton } from 'twenty-ui';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { isDefined } from '~/utils/isDefined';
import { authProvidersState } from '@/client-config/states/authProvidersState';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -15,15 +15,15 @@ const StyledContentContainer = styled.div`
`;
export const SignInUpSSOIdentityProviderSelection = () => {
const authProviders = useRecoilValue(authProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const { redirectToSSOLoginPage } = useSSO();
return (
<>
<StyledContentContainer>
{isDefined(authProviders?.sso) &&
authProviders?.sso.map((idp) => (
{isDefined(workspaceAuthProviders?.sso) &&
workspaceAuthProviders?.sso.map((idp) => (
<>
<MainButton
key={idp.id}

View File

@ -1,25 +1,25 @@
import { IconLock, MainButton, HorizontalSeparator } from 'twenty-ui';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import {
SignInUpStep,
signInUpStepState,
} from '@/auth/states/signInUpStepState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useTheme } from '@emotion/react';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useSSO } from '@/auth/sign-in-up/hooks/useSSO';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { HorizontalSeparator, IconLock, MainButton } from 'twenty-ui';
export const SignInUpWithSSO = () => {
const theme = useTheme();
const setSignInUpStep = useSetRecoilState(signInUpStepState);
const authProviders = useRecoilValue(authProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const signInUpStep = useRecoilValue(signInUpStepState);
const { redirectToSSOLoginPage } = useSSO();
const signInWithSSO = () => {
if (authProviders.sso.length === 1) {
return redirectToSSOLoginPage(authProviders.sso[0].id);
if (workspaceAuthProviders.sso.length === 1) {
return redirectToSSOLoginPage(workspaceAuthProviders.sso[0].id);
}
setSignInUpStep(SignInUpStep.SSOIdentityProviderSelection);

View File

@ -1,15 +1,15 @@
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO';
import { useHandleResetPassword } from '@/auth/sign-in-up/hooks/useHandleResetPassword';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import styled from '@emotion/styled';
import { useRecoilState } from 'recoil';
import { useRecoilValue } from 'recoil';
import { ActionLink, HorizontalSeparator } from 'twenty-ui';
import { SignInUpWithGoogle } from '@/auth/sign-in-up/components/SignInUpWithGoogle';
import { SignInUpWithMicrosoft } from '@/auth/sign-in-up/components/SignInUpWithMicrosoft';
import { SignInUpWithSSO } from '@/auth/sign-in-up/components/SignInUpWithSSO';
import { SignInUpWithCredentials } from '@/auth/sign-in-up/components/SignInUpWithCredentials';
const StyledContentContainer = styled.div`
margin-bottom: ${({ theme }) => theme.spacing(8)};
@ -17,7 +17,7 @@ const StyledContentContainer = styled.div`
`;
export const SignInUpWorkspaceScopeForm = () => {
const [authProviders] = useRecoilState(authProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const { form } = useSignInUpForm();
const { handleResetPassword } = useHandleResetPassword();
@ -27,20 +27,20 @@ export const SignInUpWorkspaceScopeForm = () => {
return (
<>
<StyledContentContainer>
{authProviders.google && <SignInUpWithGoogle />}
{workspaceAuthProviders.google && <SignInUpWithGoogle />}
{authProviders.microsoft && <SignInUpWithMicrosoft />}
{workspaceAuthProviders.microsoft && <SignInUpWithMicrosoft />}
{authProviders.sso.length > 0 && <SignInUpWithSSO />}
{workspaceAuthProviders.sso.length > 0 && <SignInUpWithSSO />}
{(authProviders.google ||
authProviders.microsoft ||
authProviders.sso.length > 0) &&
authProviders.password ? (
{(workspaceAuthProviders.google ||
workspaceAuthProviders.microsoft ||
workspaceAuthProviders.sso.length > 0) &&
workspaceAuthProviders.password ? (
<HorizontalSeparator visible />
) : null}
{authProviders.password && <SignInUpWithCredentials />}
{workspaceAuthProviders.password && <SignInUpWithCredentials />}
</StyledContentContainer>
{signInUpStep === SignInUpStep.Password && (
<ActionLink onClick={handleResetPassword(form.getValues('email'))}>

View File

@ -1,16 +1,16 @@
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { isDefined } from '~/utils/isDefined';
import { useSignInUp } from '@/auth/sign-in-up/hooks/useSignInUp';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { useSignInUpForm } from '@/auth/sign-in-up/hooks/useSignInUpForm';
import { useRecoilState } from 'recoil';
import { SignInUpStep } from '@/auth/states/signInUpStepState';
import { workspaceAuthProvidersState } from '@/workspace/states/workspaceAuthProvidersState';
import { useCallback, useEffect } from 'react';
import { useRecoilValue } from 'recoil';
import { isDefined } from '~/utils/isDefined';
const searchParams = new URLSearchParams(window.location.search);
const email = searchParams.get('email');
export const SignInUpWorkspaceScopeFormEffect = () => {
const [authProviders] = useRecoilState(authProvidersState);
const workspaceAuthProviders = useRecoilValue(workspaceAuthProvidersState);
const { form } = useSignInUpForm();
@ -20,22 +20,22 @@ export const SignInUpWorkspaceScopeFormEffect = () => {
const checkAuthProviders = useCallback(() => {
if (
signInUpStep === SignInUpStep.Init &&
!authProviders.google &&
!authProviders.microsoft &&
!authProviders.sso
!workspaceAuthProviders.google &&
!workspaceAuthProviders.microsoft &&
!workspaceAuthProviders.sso
) {
return continueWithEmail();
}
if (isDefined(email) && authProviders.password) {
if (isDefined(email) && workspaceAuthProviders.password) {
return continueWithCredentials();
}
}, [
signInUpStep,
authProviders.google,
authProviders.microsoft,
authProviders.sso,
authProviders.password,
workspaceAuthProviders.google,
workspaceAuthProviders.microsoft,
workspaceAuthProviders.sso,
workspaceAuthProviders.password,
continueWithEmail,
continueWithCredentials,
]);

View File

@ -1,4 +1,5 @@
import { apiConfigState } from '@/client-config/states/apiConfigState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { billingState } from '@/client-config/states/billingState';
import { captchaProviderState } from '@/client-config/states/captchaProviderState';
import { chromeExtensionIdState } from '@/client-config/states/chromeExtensionIdState';
@ -7,19 +8,20 @@ import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabl
import { isDebugModeState } from '@/client-config/states/isDebugModeState';
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
import { isMultiWorkspaceEnabledState } from '@/client-config/states/isMultiWorkspaceEnabledState';
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
import { sentryConfigState } from '@/client-config/states/sentryConfigState';
import { supportChatState } from '@/client-config/states/supportChatState';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { useEffect } from 'react';
import { useRecoilState, useSetRecoilState } from 'recoil';
import { useGetClientConfigQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { domainConfigurationState } from '@/domain-manager/states/domainConfigurationState';
import { isSSOEnabledState } from '@/client-config/states/isSSOEnabledState';
export const ClientConfigProviderEffect = () => {
const setIsDebugMode = useSetRecoilState(isDebugModeState);
const setIsAnalyticsEnabled = useSetRecoilState(isAnalyticsEnabledState);
const setDomainConfiguration = useSetRecoilState(domainConfigurationState);
const setAuthProviders = useSetRecoilState(authProvidersState);
const setIsDeveloperDefaultSignInPrefilled = useSetRecoilState(
isDeveloperDefaultSignInPrefilledState,
@ -73,6 +75,13 @@ export const ClientConfigProviderEffect = () => {
error: undefined,
}));
setAuthProviders({
google: data?.clientConfig.authProviders.google,
microsoft: data?.clientConfig.authProviders.microsoft,
password: data?.clientConfig.authProviders.password,
magicLink: false,
sso: data?.clientConfig.authProviders.sso,
});
setIsDebugMode(data?.clientConfig.debugMode);
setIsAnalyticsEnabled(data?.clientConfig.analyticsEnabled);
setIsDeveloperDefaultSignInPrefilled(data?.clientConfig.signInPrefilled);
@ -115,6 +124,7 @@ export const ClientConfigProviderEffect = () => {
error,
setDomainConfiguration,
setIsSSOEnabledState,
setAuthProviders,
]);
return <></>;

View File

@ -8,6 +8,18 @@ export const GET_CLIENT_CONFIG = gql`
billingUrl
billingFreeTrialDurationInDays
}
authProviders {
google
password
microsoft
sso {
id
name
type
status
issuer
}
}
signInPrefilled
isMultiWorkspaceEnabled
isSSOEnabled

View File

@ -1,17 +1,19 @@
import { useGetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { workspacePublicDataState } from '@/auth/states/workspacePublicDataState';
import { useRecoilValue, useSetRecoilState } from 'recoil';
import { useRedirectToDefaultDomain } from '@/domain-manager/hooks/useRedirectToDefaultDomain';
import { useLastAuthenticatedWorkspaceDomain } from '@/domain-manager/hooks/useLastAuthenticatedWorkspaceDomain';
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';
import { useGetPublicWorkspaceDataBySubdomainQuery } from '~/generated/graphql';
import { isDefined } from '~/utils/isDefined';
export const useGetPublicWorkspaceDataBySubdomain = () => {
const { isDefaultDomain } = useIsCurrentLocationOnDefaultDomain();
const isMultiWorkspaceEnabled = useRecoilValue(isMultiWorkspaceEnabledState);
const setAuthProviders = useSetRecoilState(authProvidersState);
const setWorkspaceAuthProviders = useSetRecoilState(
workspaceAuthProvidersState,
);
const workspacePublicData = useRecoilValue(workspacePublicDataState);
const { redirectToDefaultDomain } = useRedirectToDefaultDomain();
const setWorkspacePublicDataState = useSetRecoilState(
@ -25,7 +27,9 @@ export const useGetPublicWorkspaceDataBySubdomain = () => {
(isMultiWorkspaceEnabled && isDefaultDomain) ||
isDefined(workspacePublicData),
onCompleted: (data) => {
setAuthProviders(data.getPublicWorkspaceDataBySubdomain.authProviders);
setWorkspaceAuthProviders(
data.getPublicWorkspaceDataBySubdomain.authProviders,
);
setWorkspacePublicDataState(data.getPublicWorkspaceDataBySubdomain);
},
onError: (error) => {

View File

@ -18,6 +18,7 @@ import { ChipGeneratorsDecorator } from '~/testing/decorators/ChipGeneratorsDeco
import { MemoryRouterDecorator } from '~/testing/decorators/MemoryRouterDecorator';
import { getProfilingStory } from '~/testing/profiling/utils/getProfilingStory';
import { RecordIndexContextProvider } from '@/object-record/record-index/contexts/RecordIndexContext';
import { RecordTableBodyContextProvider } from '@/object-record/record-table/contexts/RecordTableBodyContext';
import { RecordTableContextProvider } from '@/object-record/record-table/contexts/RecordTableContext';
import { RecordTableRowContextProvider } from '@/object-record/record-table/contexts/RecordTableRowContext';
@ -63,6 +64,16 @@ const meta: Meta = {
(Story) => {
return (
<RecordFieldValueSelectorContextProvider>
<RecordIndexContextProvider
value={{
indexIdentifierUrl: (_recordId: string) => '',
onIndexRecordsLoaded: () => {},
objectNamePlural: 'companies',
objectNameSingular: 'company',
objectMetadataItem: mockPerformance.objectMetadataItem as any,
recordIndexId: 'recordIndexId',
}}
>
<RecordTableContextProvider
value={{
recordTableId: 'recordTableId',
@ -144,6 +155,7 @@ const meta: Meta = {
</RecordTableBodyContextProvider>
</RecordTableComponentInstance>
</RecordTableContextProvider>
</RecordIndexContextProvider>
</RecordFieldValueSelectorContextProvider>
);
},

View File

@ -1,4 +1,5 @@
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { authProvidersState } from '@/client-config/states/authProvidersState';
import { SettingsOptionCardContentToggle } from '@/settings/components/SettingsOptions/SettingsOptionCardContentToggle';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
@ -25,6 +26,7 @@ const StyledSettingsSecurityOptionsList = styled.div`
export const SettingsSecurityOptionsList = () => {
const { enqueueSnackBar } = useSnackBar();
const SSOIdentitiesProviders = useRecoilValue(SSOIdentitiesProvidersState);
const authProviders = useRecoilValue(authProvidersState);
const [currentWorkspace, setCurrentWorkspace] = useRecoilState(
currentWorkspaceState,
@ -123,6 +125,7 @@ export const SettingsSecurityOptionsList = () => {
{currentWorkspace && (
<>
<Card rounded>
{authProviders.google === true && (
<SettingsOptionCardContentToggle
Icon={IconGoogle}
title="Google"
@ -132,6 +135,8 @@ export const SettingsSecurityOptionsList = () => {
divider
onChange={() => toggleAuthMethod('google')}
/>
)}
{authProviders.microsoft === true && (
<SettingsOptionCardContentToggle
Icon={IconMicrosoft}
title="Microsoft"
@ -141,6 +146,8 @@ export const SettingsSecurityOptionsList = () => {
divider
onChange={() => toggleAuthMethod('microsoft')}
/>
)}
{authProviders.password === true && (
<SettingsOptionCardContentToggle
Icon={IconPassword}
title="Password"
@ -149,6 +156,7 @@ export const SettingsSecurityOptionsList = () => {
advancedMode
onChange={() => toggleAuthMethod('password')}
/>
)}
</Card>
<Card rounded>
<SettingsOptionCardContentToggle

View File

@ -0,0 +1,14 @@
import { createState } from 'twenty-ui';
import { AuthProviders } from '~/generated/graphql';
export const workspaceAuthProvidersState = createState<AuthProviders>({
key: 'workspaceAuthProvidersState',
defaultValue: {
google: true,
magicLink: false,
password: true,
microsoft: false,
sso: [],
},
});

View File

@ -1,10 +1,17 @@
import { ClientConfig } from '~/generated-metadata/graphql';
import { CaptchaDriverType } from '~/generated/graphql';
import { CaptchaDriverType, ClientConfig } from '~/generated/graphql';
export const mockedClientConfig: ClientConfig = {
signInPrefilled: true,
isMultiWorkspaceEnabled: false,
isSSOEnabled: false,
authProviders: {
google: true,
magicLink: false,
password: true,
microsoft: false,
sso: [],
__typename: 'AuthProviders',
},
frontDomain: 'localhost',
defaultSubdomain: 'app',
chromeExtensionId: 'MOCKED_EXTENSION_ID',

View File

@ -6,9 +6,10 @@ 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 { UserService } from 'src/engine/core-modules/user/services/user.service';
import { SwitchWorkspaceService } from './switch-workspace.service';
@ -50,6 +51,12 @@ describe('SwitchWorkspaceService', () => {
saveDefaultWorkspaceIfUserHasAccessOrThrow: jest.fn(),
},
},
{
provide: EnvironmentService,
useValue: {
get: jest.fn(),
},
},
],
}).compile();

View File

@ -7,13 +7,15 @@ import {
AuthException,
AuthExceptionCode,
} from 'src/engine/core-modules/auth/auth.exception';
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
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 { User } from 'src/engine/core-modules/user/user.entity';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { AuthTokens } from 'src/engine/core-modules/auth/dto/token.entity';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { UserService } from 'src/engine/core-modules/user/services/user.service';
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/getAuthProvidersByWorkspace';
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';
@Injectable()
export class SwitchWorkspaceService {
@ -25,6 +27,7 @@ export class SwitchWorkspaceService {
private readonly userService: UserService,
private readonly accessTokenService: AccessTokenService,
private readonly refreshTokenService: RefreshTokenService,
private readonly environmentService: EnvironmentService,
) {}
async switchWorkspace(user: User, workspaceId: string) {
@ -65,12 +68,23 @@ export class SwitchWorkspaceService {
defaultWorkspace: workspace,
});
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),
authProviders: getAuthProvidersByWorkspace({
workspace,
systemEnabledProviders,
}),
};
}

View File

@ -1,6 +1,7 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
@ObjectType()
class Billing {
@ -52,6 +53,9 @@ class ApiConfig {
@ObjectType()
export class ClientConfig {
@Field(() => AuthProviders, { nullable: false })
authProviders: AuthProviders;
@Field(() => Billing, { nullable: false })
billing: Billing;

View File

@ -22,6 +22,13 @@ export class ClientConfigResolver {
'BILLING_FREE_TRIAL_DURATION_IN_DAYS',
),
},
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: [],
},
isSSOEnabled: this.environmentService.get('AUTH_SSO_ENABLED'),
signInPrefilled: this.environmentService.get('SIGN_IN_PREFILLED'),
isMultiWorkspaceEnabled: this.environmentService.get(

View File

@ -23,19 +23,24 @@ export class DomainManagerService {
getFrontUrl() {
let baseUrl: URL;
const frontPort = this.environmentService.get('FRONT_PORT');
const frontDomain = this.environmentService.get('FRONT_DOMAIN');
const frontProtocol = this.environmentService.get('FRONT_PROTOCOL');
if (!this.environmentService.get('FRONT_DOMAIN')) {
baseUrl = new URL(this.environmentService.get('SERVER_URL'));
const serverUrl = this.environmentService.get('SERVER_URL');
if (!frontDomain) {
baseUrl = new URL(serverUrl);
} else {
baseUrl = new URL(
`${this.environmentService.get('FRONT_PROTOCOL')}://${this.environmentService.get('FRONT_DOMAIN')}`,
);
const port = this.environmentService.get('FRONT_PORT');
if (port) {
baseUrl.port = port.toString();
baseUrl = new URL(`${frontProtocol}://${frontDomain}`);
}
if (frontPort) {
baseUrl.port = frontPort.toString();
}
if (frontProtocol) {
baseUrl.protocol = frontProtocol;
}
return baseUrl;

View File

@ -1,7 +1,6 @@
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 { getAuthProvidersByWorkspace } from './getAuthProvidersByWorkspace';
describe('getAuthProvidersByWorkspace', () => {
const mockWorkspace = {
isGoogleAuthEnabled: true,
@ -20,7 +19,14 @@ describe('getAuthProvidersByWorkspace', () => {
it('should return correct auth providers for given workspace', () => {
const result = getAuthProvidersByWorkspace({
...mockWorkspace,
workspace: mockWorkspace,
systemEnabledProviders: {
google: true,
magicLink: false,
password: true,
microsoft: true,
sso: [],
},
});
expect(result).toEqual({
@ -42,8 +48,14 @@ describe('getAuthProvidersByWorkspace', () => {
it('should handle workspace with no SSO providers', () => {
const result = getAuthProvidersByWorkspace({
...mockWorkspace,
workspaceSSOIdentityProviders: [],
workspace: { ...mockWorkspace, workspaceSSOIdentityProviders: [] },
systemEnabledProviders: {
google: true,
magicLink: false,
password: true,
microsoft: true,
sso: [],
},
});
expect(result).toEqual({
@ -57,8 +69,14 @@ describe('getAuthProvidersByWorkspace', () => {
it('should disable Microsoft auth if isMicrosoftAuthEnabled is false', () => {
const result = getAuthProvidersByWorkspace({
...mockWorkspace,
isMicrosoftAuthEnabled: false,
workspace: { ...mockWorkspace, isMicrosoftAuthEnabled: false },
systemEnabledProviders: {
google: true,
magicLink: false,
password: true,
microsoft: true,
sso: [],
},
});
expect(result).toEqual({

View File

@ -3,13 +3,12 @@ import {
InternalServerError,
NotFoundError,
} from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
import {
WorkspaceException,
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
import { workspaceGraphqlApiExceptionHandler } from './workspaceGraphqlApiExceptionHandler';
describe('workspaceGraphqlApiExceptionHandler', () => {
it('should throw NotFoundError when WorkspaceExceptionCode is SUBDOMAIN_NOT_FOUND', () => {
const error = new WorkspaceException(

View File

@ -0,0 +1,32 @@
import { AuthProviders } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export const getAuthProvidersByWorkspace = ({
workspace,
systemEnabledProviders,
}: {
workspace: Pick<
Workspace,
| 'isGoogleAuthEnabled'
| 'isPasswordAuthEnabled'
| 'isMicrosoftAuthEnabled'
| 'workspaceSSOIdentityProviders'
>;
systemEnabledProviders: AuthProviders;
}) => {
return {
google: workspace.isGoogleAuthEnabled && systemEnabledProviders.google,
magicLink: false,
password:
workspace.isPasswordAuthEnabled && systemEnabledProviders.password,
microsoft:
workspace.isMicrosoftAuthEnabled && systemEnabledProviders.microsoft,
sso: workspace.workspaceSSOIdentityProviders.map((identityProvider) => ({
id: identityProvider.id,
name: identityProvider.name,
type: identityProvider.type,
status: identityProvider.status,
issuer: identityProvider.issuer,
})),
};
};

View File

@ -1,17 +0,0 @@
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
export const getAuthProvidersByWorkspace = (workspace: Workspace) => {
return {
google: workspace.isGoogleAuthEnabled,
magicLink: false,
password: workspace.isPasswordAuthEnabled,
microsoft: workspace.isMicrosoftAuthEnabled,
sso: workspace.workspaceSSOIdentityProviders.map((identityProvider) => ({
id: identityProvider.id,
name: identityProvider.name,
type: identityProvider.type,
status: identityProvider.status,
issuer: identityProvider.issuer,
})),
};
};

View File

@ -23,10 +23,13 @@ import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/use
import { User } from 'src/engine/core-modules/user/user.entity';
import { ActivateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-input';
import { ActivateWorkspaceOutput } from 'src/engine/core-modules/workspace/dtos/activate-workspace-output';
import { PublicWorkspaceDataOutput } from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
import {
AuthProviders,
PublicWorkspaceDataOutput,
} from 'src/engine/core-modules/workspace/dtos/public-workspace-data.output';
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/getAuthProvidersByWorkspace';
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspaceGraphqlApiExceptionHandler';
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
import {
WorkspaceException,
WorkspaceExceptionCode,
@ -207,12 +210,23 @@ export class WorkspaceResolver {
}
}
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,
logo: workspaceLogoWithToken,
displayName: workspace.displayName,
subdomain: workspace.subdomain,
authProviders: getAuthProvidersByWorkspace(workspace),
authProviders: getAuthProvidersByWorkspace({
workspace,
systemEnabledProviders,
}),
};
}
}