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:
@ -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 />} />
|
||||
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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';
|
||||
@ -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();
|
||||
@ -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))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
@ -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>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
@ -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 <></>;
|
||||
};
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,9 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_APPROVED_ACCESS_DOMAIN = gql`
|
||||
mutation DeleteApprovedAccessDomain(
|
||||
$input: DeleteApprovedAccessDomainInput!
|
||||
) {
|
||||
deleteApprovedAccessDomain(input: $input)
|
||||
}
|
||||
`;
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -0,0 +1,12 @@
|
||||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ALL_APPROVED_ACCESS_DOMAINS = gql`
|
||||
query GetApprovedAccessDomains {
|
||||
getApprovedAccessDomains {
|
||||
id
|
||||
createdAt
|
||||
domain
|
||||
isValidated
|
||||
}
|
||||
}
|
||||
`;
|
||||
@ -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: [],
|
||||
});
|
||||
@ -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',
|
||||
|
||||
Reference in New Issue
Block a user