feat(twenty-server): add trusted domain - backend crud (#10290)

Co-authored-by: Etienne <45695613+etiennejouan@users.noreply.github.com>
Co-authored-by: Paul Rastoin <45004772+prastoin@users.noreply.github.com>
This commit is contained in:
Antoine Moreaux
2025-02-21 17:02:48 +01:00
committed by GitHub
parent 22203bfd3c
commit bf92860d19
49 changed files with 1812 additions and 147 deletions

View File

@ -4,11 +4,14 @@ import { H2Title, IconLock, Section, Tag } from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsReadDocumentationButton } from '@/settings/developers/components/SettingsReadDocumentationButton';
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCard';
import { SettingsSecurityOptionsList } from '@/settings/security/components/SettingsSecurityOptionsList';
import { SettingsSSOIdentitiesProvidersListCard } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCard';
import { SettingsSecurityAuthProvidersOptionsList } from '@/settings/security/components/SettingsSecurityAuthProvidersOptionsList';
import { SettingsPath } from '@/types/SettingsPath';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
import { FeatureFlagKey } from '~/generated/graphql';
const StyledContainer = styled.div`
width: 100%;
@ -21,13 +24,17 @@ const StyledMainContent = styled.div`
min-height: 200px;
`;
const StyledSSOSection = styled(Section)`
const StyledSection = styled(Section)`
flex-shrink: 0;
`;
export const SettingsSecurity = () => {
const { t } = useLingui();
const IsApprovedAccessDomainsEnabled = useIsFeatureEnabled(
FeatureFlagKey.IsApprovedAccessDomainsEnabled,
);
return (
<SubMenuTopBarContainer
title={t`Security`}
@ -42,7 +49,7 @@ export const SettingsSecurity = () => {
>
<SettingsPageContainer>
<StyledMainContent>
<StyledSSOSection>
<StyledSection>
<H2Title
title={t`SSO`}
description={t`Configure an SSO connection`}
@ -56,14 +63,23 @@ export const SettingsSecurity = () => {
}
/>
<SettingsSSOIdentitiesProvidersListCard />
</StyledSSOSection>
</StyledSection>
{IsApprovedAccessDomainsEnabled && (
<StyledSection>
<H2Title
title={t`Approved Email Domain`}
description={t`Anyone with an email address at these domains is allowed to sign up for this workspace.`}
/>
<SettingsApprovedAccessDomainsListCard />
</StyledSection>
)}
<Section>
<StyledContainer>
<H2Title
title={t`Authentication`}
description={t`Customize your workspace security`}
/>
<SettingsSecurityOptionsList />
<SettingsSecurityAuthProvidersOptionsList />
</StyledContainer>
</Section>
</StyledMainContent>

View File

@ -0,0 +1,152 @@
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import { SettingsPath } from '@/types/SettingsPath';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { zodResolver } from '@hookform/resolvers/zod';
import { Controller, useForm } from 'react-hook-form';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { Trans, useLingui } from '@lingui/react/macro';
import { TextInput } from '@/ui/input/components/TextInput';
import { z } from 'zod';
import { H2Title, Section } from 'twenty-ui';
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { useCreateApprovedAccessDomainMutation } from '~/generated/graphql';
export const SettingsSecurityApprovedAccessDomain = () => {
const navigate = useNavigateSettings();
const { t } = useLingui();
const { enqueueSnackBar } = useSnackBar();
const [createApprovedAccessDomain] = useCreateApprovedAccessDomainMutation();
const formConfig = useForm<{ domain: string; email: string }>({
mode: 'onSubmit',
resolver: zodResolver(
z
.object({
domain: z
.string()
.regex(
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/,
{
message: t`Invalid domain. Domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
},
)
.max(256),
email: z.string().min(1, {
message: t`Email can not be empty`,
}),
})
.strict(),
),
defaultValues: {
email: '',
domain: '',
},
});
const domain = formConfig.watch('domain');
const handleSave = async () => {
try {
if (!formConfig.formState.isValid) {
return;
}
createApprovedAccessDomain({
variables: {
input: {
domain: formConfig.getValues('domain'),
email:
formConfig.getValues('email') +
'@' +
formConfig.getValues('domain'),
},
},
onCompleted: () => {
enqueueSnackBar(t`Domain added successfully.`, {
variant: SnackBarVariant.Success,
});
navigate(SettingsPath.Security);
},
onError: (error) => {
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
});
},
});
} catch (error) {
enqueueSnackBar((error as Error).message, {
variant: SnackBarVariant.Error,
});
}
};
return (
<SubMenuTopBarContainer
title="New Approved Access Domain"
actionButton={
<SaveAndCancelButtons
onCancel={() => navigate(SettingsPath.Security)}
onSave={formConfig.handleSubmit(handleSave)}
/>
}
links={[
{
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: <Trans>Security</Trans>,
href: getSettingsPath(SettingsPath.Security),
},
{ children: <Trans>New Approved Access Domain</Trans> },
]}
>
<SettingsPageContainer>
<Section>
<H2Title title="Domain" description="The name of your Domain" />
<Controller
name="domain"
control={formConfig.control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<TextInput
autoComplete="off"
value={value}
onChange={(domain: string) => {
onChange(domain);
}}
fullWidth
placeholder="yourdomain.com"
error={error?.message}
/>
)}
/>
</Section>
<Section>
<H2Title
title="Email verification"
description="We will send your a link to verify domain ownership"
/>
<Controller
name="email"
control={formConfig.control}
render={({ field: { onChange, value }, fieldState: { error } }) => (
<TextInput
autoComplete="off"
value={value.split('@')[0]}
onChange={onChange}
fullWidth
error={error?.message}
/>
)}
/>
{domain}
</Section>
</SettingsPageContainer>
</SubMenuTopBarContainer>
);
};

View File

@ -1,7 +1,7 @@
/* @license Enterprise */
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
import SettingsSSOIdentitiesProvidersForm from '@/settings/security/components/SettingsSSOIdentitiesProvidersForm';
import SettingsSSOIdentitiesProvidersForm from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersForm';
import { useCreateSSOIdentityProvider } from '@/settings/security/hooks/useCreateSSOIdentityProvider';
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
import { sSOIdentityProviderDefaultValues } from '@/settings/security/utils/sSOIdentityProviderDefaultValues';
@ -11,6 +11,7 @@ import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/Snac
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
import { zodResolver } from '@hookform/resolvers/zod';
import { Trans } from '@lingui/react/macro';
import pick from 'lodash.pick';
import { FormProvider, useForm } from 'react-hook-form';
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
@ -64,14 +65,14 @@ export const SettingsSecuritySSOIdentifyProvider = () => {
}
links={[
{
children: 'Workspace',
children: <Trans>Workspace</Trans>,
href: getSettingsPath(SettingsPath.Workspace),
},
{
children: 'Security',
children: <Trans>Security</Trans>,
href: getSettingsPath(SettingsPath.Security),
},
{ children: 'New' },
{ children: <Trans>New SSO provider</Trans> },
]}
>
<FormProvider

View File

@ -45,7 +45,7 @@ export const SettingsDomain = () => {
.regex(
/^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/,
{
message: t`Invalid custom domain. Custom domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
message: t`Invalid domain. Domains have to be smaller than 256 characters in length, cannot be IP addresses, cannot contain spaces, cannot contain any special characters such as _~\`!@#$%^*()=+{}[]|\\;:'",<>/? and cannot begin or end with a '-' character.`,
},
)
.max(256)