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

@ -234,6 +234,14 @@ const SettingsSecuritySSOIdentifyProvider = lazy(() =>
),
);
const SettingsSecurityApprovedAccessDomain = lazy(() =>
import('~/pages/settings/security/SettingsSecurityApprovedAccessDomain').then(
(module) => ({
default: module.SettingsSecurityApprovedAccessDomain,
}),
),
);
const SettingsAdmin = lazy(() =>
import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({
default: module.SettingsAdmin,
@ -408,6 +416,11 @@ export const SettingsRoutes = ({
path={SettingsPath.NewSSOIdentityProvider}
element={<SettingsSecuritySSOIdentifyProvider />}
/>
<Route
path={SettingsPath.NewApprovedAccessDomain}
element={<SettingsSecurityApprovedAccessDomain />}
/>
{isAdminPageEnabled && (
<>
<Route path={SettingsPath.AdminPanel} element={<SettingsAdmin />} />

View File

@ -2,8 +2,8 @@
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
import { SettingsRadioCardContainer } from '@/settings/components/SettingsRadioCardContainer';
import { SettingsSSOOIDCForm } from '@/settings/security/components/SettingsSSOOIDCForm';
import { SettingsSSOSAMLForm } from '@/settings/security/components/SettingsSSOSAMLForm';
import { SettingsSSOOIDCForm } from '@/settings/security/components/SSO/SettingsSSOOIDCForm';
import { SettingsSSOSAMLForm } from '@/settings/security/components/SSO/SettingsSSOSAMLForm';
import { SettingSecurityNewSSOIdentityFormValues } from '@/settings/security/types/SSOIdentityProvider';
import { TextInput } from '@/ui/input/components/TextInput';
import styled from '@emotion/styled';

View File

@ -6,7 +6,7 @@ import { SettingsPath } from '@/types/SettingsPath';
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SettingsSSOIdentitiesProvidersListCardWrapper';
import { SettingsSSOIdentitiesProvidersListCardWrapper } from '@/settings/security/components/SSO/SettingsSSOIdentitiesProvidersListCardWrapper';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';

View File

@ -1,7 +1,7 @@
/* @license Enterprise */
import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/security/components/SettingsSSOIdentityProviderRowRightContainer';
import { SettingsSSOIdentityProviderRowRightContainer } from '@/settings/security/components/SSO/SettingsSSOIdentityProviderRowRightContainer';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { guessSSOIdentityProviderIconByUrl } from '@/settings/security/utils/guessSSOIdentityProviderIconByUrl';
import { SettingsPath } from '@/types/SettingsPath';

View File

@ -1,6 +1,6 @@
/* @license Enterprise */
import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SettingsSecuritySSORowDropdownMenu';
import { SettingsSecuritySSORowDropdownMenu } from '@/settings/security/components/SSO/SettingsSecuritySSORowDropdownMenu';
import { SSOIdentitiesProvidersState } from '@/settings/security/states/SSOIdentitiesProvidersState';
import { getColorBySSOIdentityProviderStatus } from '@/settings/security/utils/getColorBySSOIdentityProviderStatus';
import { Status } from 'twenty-ui';

View File

@ -24,7 +24,7 @@ const StyledSettingsSecurityOptionsList = styled.div`
gap: ${({ theme }) => theme.spacing(4)};
`;
export const SettingsSecurityOptionsList = () => {
export const SettingsSecurityAuthProvidersOptionsList = () => {
const { t } = useLingui();
const { enqueueSnackBar } = useSnackBar();

View File

@ -0,0 +1,73 @@
import { Link, useNavigate } from 'react-router-dom';
import { SettingsPath } from '@/types/SettingsPath';
import { SettingsCard } from '@/settings/components/SettingsCard';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import styled from '@emotion/styled';
import { useLingui } from '@lingui/react/macro';
import { useRecoilState } from 'recoil';
import { IconAt, IconMailCog } from 'twenty-ui';
import { getSettingsPath } from '~/utils/navigation/getSettingsPath';
import { SettingsListCard } from '@/settings/components/SettingsListCard';
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
import { SettingsSecurityApprovedAccessDomainRowDropdownMenu } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainRowDropdownMenu';
import { SettingsSecurityApprovedAccessDomainValidationEffect } from '@/settings/security/components/approvedAccessDomains/SettingsSecurityApprovedAccessDomainValidationEffect';
import { useGetApprovedAccessDomainsQuery } from '~/generated/graphql';
const StyledLink = styled(Link)`
text-decoration: none;
`;
export const SettingsApprovedAccessDomainsListCard = () => {
const { enqueueSnackBar } = useSnackBar();
const navigate = useNavigate();
const { t } = useLingui();
const [approvedAccessDomains, setApprovedAccessDomains] = useRecoilState(
approvedAccessDomainsState,
);
const { loading } = useGetApprovedAccessDomainsQuery({
fetchPolicy: 'network-only',
onCompleted: (data) => {
setApprovedAccessDomains(data?.getApprovedAccessDomains ?? []);
},
onError: (error: Error) => {
enqueueSnackBar(error.message, {
variant: SnackBarVariant.Error,
});
},
});
return loading || !approvedAccessDomains.length ? (
<StyledLink to={getSettingsPath(SettingsPath.NewApprovedAccessDomain)}>
<SettingsCard
title={t`Add Approved Access Domain`}
Icon={<IconMailCog />}
/>
</StyledLink>
) : (
<>
<SettingsSecurityApprovedAccessDomainValidationEffect />
<SettingsListCard
items={approvedAccessDomains}
getItemLabel={(approvedAccessDomain) =>
`${approvedAccessDomain.domain} - ${approvedAccessDomain.createdAt}`
}
RowIcon={IconAt}
RowRightComponent={({ item: approvedAccessDomain }) => (
<SettingsSecurityApprovedAccessDomainRowDropdownMenu
approvedAccessDomain={approvedAccessDomain}
/>
)}
hasFooter
footerButtonLabel="Add Approved Access Domain"
onFooterButtonClick={() =>
navigate(getSettingsPath(SettingsPath.NewApprovedAccessDomain))
}
/>
</>
);
};

View File

@ -0,0 +1,84 @@
import {
IconDotsVertical,
IconTrash,
LightIconButton,
MenuItem,
} from 'twenty-ui';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
import { useDropdown } from '@/ui/layout/dropdown/hooks/useDropdown';
import { UnwrapRecoilValue, useSetRecoilState } from 'recoil';
import { isDefined } from 'twenty-shared';
import { useDeleteApprovedAccessDomainMutation } from '~/generated/graphql';
import { approvedAccessDomainsState } from '@/settings/security/states/ApprovedAccessDomainsState';
type SettingsSecurityApprovedAccessDomainRowDropdownMenuProps = {
approvedAccessDomain: UnwrapRecoilValue<typeof approvedAccessDomainsState>[0];
};
export const SettingsSecurityApprovedAccessDomainRowDropdownMenu = ({
approvedAccessDomain,
}: SettingsSecurityApprovedAccessDomainRowDropdownMenuProps) => {
const dropdownId = `settings-approved-access-domain-row-${approvedAccessDomain.id}`;
const setApprovedAccessDomains = useSetRecoilState(
approvedAccessDomainsState,
);
const { enqueueSnackBar } = useSnackBar();
const { closeDropdown } = useDropdown(dropdownId);
const [deleteApprovedAccessDomain] = useDeleteApprovedAccessDomainMutation();
const handleDeleteApprovedAccessDomain = async () => {
const result = await deleteApprovedAccessDomain({
variables: {
input: {
id: approvedAccessDomain.id,
},
},
onCompleted: () => {
setApprovedAccessDomains((approvedAccessDomains) => {
return approvedAccessDomains.filter(
({ id }) => id !== approvedAccessDomain.id,
);
});
},
});
if (isDefined(result.errors)) {
enqueueSnackBar('Error deleting approved access domain', {
variant: SnackBarVariant.Error,
duration: 2000,
});
}
};
return (
<Dropdown
dropdownId={dropdownId}
dropdownPlacement="right-start"
dropdownHotkeyScope={{ scope: dropdownId }}
clickableComponent={
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
}
dropdownMenuWidth={160}
dropdownComponents={
<DropdownMenuItemsContainer>
<MenuItem
accent="danger"
LeftIcon={IconTrash}
text="Delete"
onClick={() => {
handleDeleteApprovedAccessDomain();
closeDropdown();
}}
/>
</DropdownMenuItemsContainer>
}
/>
);
};

View File

@ -0,0 +1,44 @@
import { useEffect } from 'react';
import { isDefined } from 'twenty-shared';
import { useValidateApprovedAccessDomainMutation } from '~/generated/graphql';
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
import { useSearchParams } from 'react-router-dom';
import { SnackBarVariant } from '@/ui/feedback/snack-bar-manager/components/SnackBar';
export const SettingsSecurityApprovedAccessDomainValidationEffect = () => {
const [validateApprovedAccessDomainMutation] =
useValidateApprovedAccessDomainMutation();
const { enqueueSnackBar } = useSnackBar();
const [searchParams] = useSearchParams();
const approvedAccessDomainId = searchParams.get('wtdId');
const validationToken = searchParams.get('validationToken');
useEffect(() => {
if (isDefined(validationToken) && isDefined(approvedAccessDomainId)) {
validateApprovedAccessDomainMutation({
variables: {
input: {
validationToken,
approvedAccessDomainId,
},
},
onCompleted: () => {
enqueueSnackBar('Approved access domain validated', {
dedupeKey: 'approved-access-domain-validation-dedupe-key',
variant: SnackBarVariant.Success,
});
},
onError: () => {
enqueueSnackBar('Error validating approved access domain', {
dedupeKey: 'approved-access-domain-validation-error-dedupe-key',
variant: SnackBarVariant.Error,
});
},
});
}
// Validate approved access domain only needs to run once at mount
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return <></>;
};

View File

@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const CREATE_APPROVED_ACCESS_DOMAIN = gql`
mutation CreateApprovedAccessDomain(
$input: CreateApprovedAccessDomainInput!
) {
createApprovedAccessDomain(input: $input) {
id
domain
isValidated
createdAt
}
}
`;

View File

@ -0,0 +1,9 @@
import { gql } from '@apollo/client';
export const DELETE_APPROVED_ACCESS_DOMAIN = gql`
mutation DeleteApprovedAccessDomain(
$input: DeleteApprovedAccessDomainInput!
) {
deleteApprovedAccessDomain(input: $input)
}
`;

View File

@ -0,0 +1,14 @@
import { gql } from '@apollo/client';
export const VALIDATE_APPROVED_ACCESS_DOMAIN = gql`
mutation ValidateApprovedAccessDomain(
$input: ValidateApprovedAccessDomainInput!
) {
validateApprovedAccessDomain(input: $input) {
id
isValidated
domain
createdAt
}
}
`;

View File

@ -0,0 +1,12 @@
import { gql } from '@apollo/client';
export const GET_ALL_APPROVED_ACCESS_DOMAINS = gql`
query GetApprovedAccessDomains {
getApprovedAccessDomains {
id
createdAt
domain
isValidated
}
}
`;

View File

@ -0,0 +1,9 @@
import { createState } from '@ui/utilities/state/utils/createState';
import { ApprovedAccessDomain } from '~/generated/graphql';
export const approvedAccessDomainsState = createState<
Omit<ApprovedAccessDomain, '__typename'>[]
>({
key: 'ApprovedAccessDomainsState',
defaultValue: [],
});

View File

@ -29,7 +29,9 @@ export enum SettingsPath {
IntegrationNewDatabaseConnection = 'integrations/:databaseKey/new',
Security = 'security',
NewSSOIdentityProvider = 'security/sso/new',
EditSSOIdentityProvider = 'security/sso/:identityProviderId',
NewApprovedAccessDomain = 'security/approved-access-domain/new',
Webhooks = 'webhooks',
DevelopersNewWebhook = 'developers/webhooks/new',
DevelopersNewWebhookDetail = 'developers/webhooks/:webhookId',
Releases = 'releases',
AdminPanel = 'admin-panel',