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

@ -102,6 +102,14 @@ export type AppTokenEdge = {
node: AppToken;
};
export type ApprovedAccessDomain = {
__typename?: 'ApprovedAccessDomain';
createdAt: Scalars['DateTime']['output'];
domain: Scalars['String']['output'];
id: Scalars['UUID']['output'];
isValidated: Scalars['Boolean']['output'];
};
export type AuthProviders = {
__typename?: 'AuthProviders';
google: Scalars['Boolean']['output'];
@ -300,6 +308,11 @@ export type CreateAppTokenInput = {
expiresAt: Scalars['DateTime']['input'];
};
export type CreateApprovedAccessDomainInput = {
domain: Scalars['String']['input'];
email: Scalars['String']['input'];
};
export type CreateDraftFromWorkflowVersionInput = {
/** Workflow ID */
workflowId: Scalars['String']['input'];
@ -423,6 +436,10 @@ export type CustomDomainValidRecords = {
records: Array<CustomDomainRecord>;
};
export type DeleteApprovedAccessDomainInput = {
id: Scalars['String']['input'];
};
export type DeleteOneFieldInput = {
/** The id of the field to delete. */
id: Scalars['UUID']['input'];
@ -545,6 +562,7 @@ export enum FeatureFlagKey {
IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled',
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
IsCopilotEnabled = 'IsCopilotEnabled',
@ -833,6 +851,7 @@ export type Mutation = {
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
checkoutSession: BillingSessionOutput;
computeStepOutputSchema: Scalars['JSON']['output'];
createApprovedAccessDomain: ApprovedAccessDomain;
createDraftFromWorkflowVersion: WorkflowVersion;
createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
@ -844,6 +863,7 @@ export type Mutation = {
createSAMLIdentityProvider: SetupSsoOutput;
createWorkflowVersionStep: WorkflowAction;
deactivateWorkflowVersion: Scalars['Boolean']['output'];
deleteApprovedAccessDomain: Scalars['Boolean']['output'];
deleteCurrentWorkspace: Workspace;
deleteOneField: Field;
deleteOneObject: Object;
@ -894,6 +914,7 @@ export type Mutation = {
uploadProfilePicture: Scalars['String']['output'];
uploadWorkspaceLogo: Scalars['String']['output'];
userLookupAdminPanel: UserLookup;
validateApprovedAccessDomain: ApprovedAccessDomain;
};
@ -927,6 +948,11 @@ export type MutationComputeStepOutputSchemaArgs = {
};
export type MutationCreateApprovedAccessDomainArgs = {
input: CreateApprovedAccessDomainInput;
};
export type MutationCreateDraftFromWorkflowVersionArgs = {
input: CreateDraftFromWorkflowVersionInput;
};
@ -982,6 +1008,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
};
export type MutationDeleteApprovedAccessDomainArgs = {
input: DeleteApprovedAccessDomainInput;
};
export type MutationDeleteOneFieldArgs = {
input: DeleteOneFieldInput;
};
@ -1214,6 +1245,11 @@ export type MutationUserLookupAdminPanelArgs = {
userIdentifier: Scalars['String']['input'];
};
export type MutationValidateApprovedAccessDomainArgs = {
input: ValidateApprovedAccessDomainInput;
};
export type Object = {
__typename?: 'Object';
createdAt: Scalars['DateTime']['output'];
@ -1382,6 +1418,7 @@ export type Query = {
findOneServerlessFunction: ServerlessFunction;
findWorkspaceFromInviteHash: Workspace;
findWorkspaceInvitations: Array<WorkspaceInvitation>;
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
getAvailablePackages: Scalars['JSON']['output'];
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
getIndicatorHealthStatus: AdminPanelHealthServiceData;
@ -2101,6 +2138,11 @@ export type UserWorkspace = {
workspaceId: Scalars['String']['output'];
};
export type ValidateApprovedAccessDomainInput = {
approvedAccessDomainId: Scalars['String']['input'];
validationToken: Scalars['String']['input'];
};
export type ValidatePasswordResetToken = {
__typename?: 'ValidatePasswordResetToken';
email: Scalars['String']['output'];

View File

@ -92,6 +92,14 @@ export type AppTokenEdge = {
node: AppToken;
};
export type ApprovedAccessDomain = {
__typename?: 'ApprovedAccessDomain';
createdAt: Scalars['DateTime'];
domain: Scalars['String'];
id: Scalars['UUID'];
isValidated: Scalars['Boolean'];
};
export type AuthProviders = {
__typename?: 'AuthProviders';
google: Scalars['Boolean'];
@ -286,6 +294,11 @@ export type ComputeStepOutputSchemaInput = {
step: Scalars['JSON'];
};
export type CreateApprovedAccessDomainInput = {
domain: Scalars['String'];
email: Scalars['String'];
};
export type CreateDraftFromWorkflowVersionInput = {
/** Workflow ID */
workflowId: Scalars['String'];
@ -357,6 +370,10 @@ export type CustomDomainValidRecords = {
records: Array<CustomDomainRecord>;
};
export type DeleteApprovedAccessDomainInput = {
id: Scalars['String'];
};
export type DeleteOneFieldInput = {
/** The id of the field to delete. */
id: Scalars['UUID'];
@ -474,6 +491,7 @@ export enum FeatureFlagKey {
IsAdvancedFiltersEnabled = 'IsAdvancedFiltersEnabled',
IsAirtableIntegrationEnabled = 'IsAirtableIntegrationEnabled',
IsAnalyticsV2Enabled = 'IsAnalyticsV2Enabled',
IsApprovedAccessDomainsEnabled = 'IsApprovedAccessDomainsEnabled',
IsBillingPlansEnabled = 'IsBillingPlansEnabled',
IsCommandMenuV2Enabled = 'IsCommandMenuV2Enabled',
IsCopilotEnabled = 'IsCopilotEnabled',
@ -762,6 +780,7 @@ export type Mutation = {
checkCustomDomainValidRecords?: Maybe<CustomDomainValidRecords>;
checkoutSession: BillingSessionOutput;
computeStepOutputSchema: Scalars['JSON'];
createApprovedAccessDomain: ApprovedAccessDomain;
createDraftFromWorkflowVersion: WorkflowVersion;
createOIDCIdentityProvider: SetupSsoOutput;
createOneAppToken: AppToken;
@ -771,6 +790,7 @@ export type Mutation = {
createSAMLIdentityProvider: SetupSsoOutput;
createWorkflowVersionStep: WorkflowAction;
deactivateWorkflowVersion: Scalars['Boolean'];
deleteApprovedAccessDomain: Scalars['Boolean'];
deleteCurrentWorkspace: Workspace;
deleteOneField: Field;
deleteOneObject: Object;
@ -815,6 +835,7 @@ export type Mutation = {
uploadProfilePicture: Scalars['String'];
uploadWorkspaceLogo: Scalars['String'];
userLookupAdminPanel: UserLookup;
validateApprovedAccessDomain: ApprovedAccessDomain;
};
@ -848,6 +869,11 @@ export type MutationComputeStepOutputSchemaArgs = {
};
export type MutationCreateApprovedAccessDomainArgs = {
input: CreateApprovedAccessDomainInput;
};
export type MutationCreateDraftFromWorkflowVersionArgs = {
input: CreateDraftFromWorkflowVersionInput;
};
@ -883,6 +909,11 @@ export type MutationDeactivateWorkflowVersionArgs = {
};
export type MutationDeleteApprovedAccessDomainArgs = {
input: DeleteApprovedAccessDomainInput;
};
export type MutationDeleteOneFieldArgs = {
input: DeleteOneFieldInput;
};
@ -1085,6 +1116,11 @@ export type MutationUserLookupAdminPanelArgs = {
userIdentifier: Scalars['String'];
};
export type MutationValidateApprovedAccessDomainArgs = {
input: ValidateApprovedAccessDomainInput;
};
export type Object = {
__typename?: 'Object';
createdAt: Scalars['DateTime'];
@ -1250,6 +1286,7 @@ export type Query = {
findOneServerlessFunction: ServerlessFunction;
findWorkspaceFromInviteHash: Workspace;
findWorkspaceInvitations: Array<WorkspaceInvitation>;
getApprovedAccessDomains: Array<ApprovedAccessDomain>;
getAvailablePackages: Scalars['JSON'];
getEnvironmentVariablesGrouped: EnvironmentVariablesOutput;
getIndicatorHealthStatus: AdminPanelHealthServiceData;
@ -1887,6 +1924,11 @@ export type UserWorkspace = {
workspaceId: Scalars['String'];
};
export type ValidateApprovedAccessDomainInput = {
approvedAccessDomainId: Scalars['String'];
validationToken: Scalars['String'];
};
export type ValidatePasswordResetToken = {
__typename?: 'ValidatePasswordResetToken';
email: Scalars['String'];
@ -2333,6 +2375,13 @@ export type GetRolesQueryVariables = Exact<{ [key: string]: never; }>;
export type GetRolesQuery = { __typename?: 'Query', getRoles: Array<{ __typename?: 'Role', id: string, label: string, description?: string | null, canUpdateAllSettings: boolean, isEditable: boolean, canReadAllObjectRecords: boolean, canUpdateAllObjectRecords: boolean, canSoftDeleteAllObjectRecords: boolean, canDestroyAllObjectRecords: boolean, workspaceMembers: Array<{ __typename?: 'WorkspaceMember', id: any, colorScheme: string, avatarUrl?: string | null, locale?: string | null, userEmail: string, timeZone?: string | null, dateFormat?: WorkspaceMemberDateFormatEnum | null, timeFormat?: WorkspaceMemberTimeFormatEnum | null, name: { __typename?: 'FullName', firstName: string, lastName: string } }> }> };
export type CreateApprovedAccessDomainMutationVariables = Exact<{
input: CreateApprovedAccessDomainInput;
}>;
export type CreateApprovedAccessDomainMutation = { __typename?: 'Mutation', createApprovedAccessDomain: { __typename?: 'ApprovedAccessDomain', id: any, domain: string, isValidated: boolean, createdAt: string } };
export type CreateOidcIdentityProviderMutationVariables = Exact<{
input: SetupOidcSsoInput;
}>;
@ -2347,6 +2396,13 @@ export type CreateSamlIdentityProviderMutationVariables = Exact<{
export type CreateSamlIdentityProviderMutation = { __typename?: 'Mutation', createSAMLIdentityProvider: { __typename?: 'SetupSsoOutput', id: string, type: IdentityProviderType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
export type DeleteApprovedAccessDomainMutationVariables = Exact<{
input: DeleteApprovedAccessDomainInput;
}>;
export type DeleteApprovedAccessDomainMutation = { __typename?: 'Mutation', deleteApprovedAccessDomain: boolean };
export type DeleteSsoIdentityProviderMutationVariables = Exact<{
input: DeleteSsoInput;
}>;
@ -2361,6 +2417,18 @@ export type EditSsoIdentityProviderMutationVariables = Exact<{
export type EditSsoIdentityProviderMutation = { __typename?: 'Mutation', editSSOIdentityProvider: { __typename?: 'EditSsoOutput', id: string, type: IdentityProviderType, issuer: string, name: string, status: SsoIdentityProviderStatus } };
export type ValidateApprovedAccessDomainMutationVariables = Exact<{
input: ValidateApprovedAccessDomainInput;
}>;
export type ValidateApprovedAccessDomainMutation = { __typename?: 'Mutation', validateApprovedAccessDomain: { __typename?: 'ApprovedAccessDomain', id: any, isValidated: boolean, domain: string, createdAt: string } };
export type GetApprovedAccessDomainsQueryVariables = Exact<{ [key: string]: never; }>;
export type GetApprovedAccessDomainsQuery = { __typename?: 'Query', getApprovedAccessDomains: Array<{ __typename?: 'ApprovedAccessDomain', id: any, createdAt: string, domain: string, isValidated: boolean }> };
export type GetSsoIdentityProvidersQueryVariables = Exact<{ [key: string]: never; }>;
@ -4230,6 +4298,42 @@ export function useGetRolesLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<G
export type GetRolesQueryHookResult = ReturnType<typeof useGetRolesQuery>;
export type GetRolesLazyQueryHookResult = ReturnType<typeof useGetRolesLazyQuery>;
export type GetRolesQueryResult = Apollo.QueryResult<GetRolesQuery, GetRolesQueryVariables>;
export const CreateApprovedAccessDomainDocument = gql`
mutation CreateApprovedAccessDomain($input: CreateApprovedAccessDomainInput!) {
createApprovedAccessDomain(input: $input) {
id
domain
isValidated
createdAt
}
}
`;
export type CreateApprovedAccessDomainMutationFn = Apollo.MutationFunction<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>;
/**
* __useCreateApprovedAccessDomainMutation__
*
* To run a mutation, you first call `useCreateApprovedAccessDomainMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useCreateApprovedAccessDomainMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [createApprovedAccessDomainMutation, { data, loading, error }] = useCreateApprovedAccessDomainMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useCreateApprovedAccessDomainMutation(baseOptions?: Apollo.MutationHookOptions<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>(CreateApprovedAccessDomainDocument, options);
}
export type CreateApprovedAccessDomainMutationHookResult = ReturnType<typeof useCreateApprovedAccessDomainMutation>;
export type CreateApprovedAccessDomainMutationResult = Apollo.MutationResult<CreateApprovedAccessDomainMutation>;
export type CreateApprovedAccessDomainMutationOptions = Apollo.BaseMutationOptions<CreateApprovedAccessDomainMutation, CreateApprovedAccessDomainMutationVariables>;
export const CreateOidcIdentityProviderDocument = gql`
mutation CreateOIDCIdentityProvider($input: SetupOIDCSsoInput!) {
createOIDCIdentityProvider(input: $input) {
@ -4304,6 +4408,37 @@ export function useCreateSamlIdentityProviderMutation(baseOptions?: Apollo.Mutat
export type CreateSamlIdentityProviderMutationHookResult = ReturnType<typeof useCreateSamlIdentityProviderMutation>;
export type CreateSamlIdentityProviderMutationResult = Apollo.MutationResult<CreateSamlIdentityProviderMutation>;
export type CreateSamlIdentityProviderMutationOptions = Apollo.BaseMutationOptions<CreateSamlIdentityProviderMutation, CreateSamlIdentityProviderMutationVariables>;
export const DeleteApprovedAccessDomainDocument = gql`
mutation DeleteApprovedAccessDomain($input: DeleteApprovedAccessDomainInput!) {
deleteApprovedAccessDomain(input: $input)
}
`;
export type DeleteApprovedAccessDomainMutationFn = Apollo.MutationFunction<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>;
/**
* __useDeleteApprovedAccessDomainMutation__
*
* To run a mutation, you first call `useDeleteApprovedAccessDomainMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useDeleteApprovedAccessDomainMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [deleteApprovedAccessDomainMutation, { data, loading, error }] = useDeleteApprovedAccessDomainMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useDeleteApprovedAccessDomainMutation(baseOptions?: Apollo.MutationHookOptions<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>(DeleteApprovedAccessDomainDocument, options);
}
export type DeleteApprovedAccessDomainMutationHookResult = ReturnType<typeof useDeleteApprovedAccessDomainMutation>;
export type DeleteApprovedAccessDomainMutationResult = Apollo.MutationResult<DeleteApprovedAccessDomainMutation>;
export type DeleteApprovedAccessDomainMutationOptions = Apollo.BaseMutationOptions<DeleteApprovedAccessDomainMutation, DeleteApprovedAccessDomainMutationVariables>;
export const DeleteSsoIdentityProviderDocument = gql`
mutation DeleteSSOIdentityProvider($input: DeleteSsoInput!) {
deleteSSOIdentityProvider(input: $input) {
@ -4374,6 +4509,79 @@ export function useEditSsoIdentityProviderMutation(baseOptions?: Apollo.Mutation
export type EditSsoIdentityProviderMutationHookResult = ReturnType<typeof useEditSsoIdentityProviderMutation>;
export type EditSsoIdentityProviderMutationResult = Apollo.MutationResult<EditSsoIdentityProviderMutation>;
export type EditSsoIdentityProviderMutationOptions = Apollo.BaseMutationOptions<EditSsoIdentityProviderMutation, EditSsoIdentityProviderMutationVariables>;
export const ValidateApprovedAccessDomainDocument = gql`
mutation ValidateApprovedAccessDomain($input: ValidateApprovedAccessDomainInput!) {
validateApprovedAccessDomain(input: $input) {
id
isValidated
domain
createdAt
}
}
`;
export type ValidateApprovedAccessDomainMutationFn = Apollo.MutationFunction<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>;
/**
* __useValidateApprovedAccessDomainMutation__
*
* To run a mutation, you first call `useValidateApprovedAccessDomainMutation` within a React component and pass it any options that fit your needs.
* When your component renders, `useValidateApprovedAccessDomainMutation` returns a tuple that includes:
* - A mutate function that you can call at any time to execute the mutation
* - An object with fields that represent the current status of the mutation's execution
*
* @param baseOptions options that will be passed into the mutation, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options-2;
*
* @example
* const [validateApprovedAccessDomainMutation, { data, loading, error }] = useValidateApprovedAccessDomainMutation({
* variables: {
* input: // value for 'input'
* },
* });
*/
export function useValidateApprovedAccessDomainMutation(baseOptions?: Apollo.MutationHookOptions<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useMutation<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>(ValidateApprovedAccessDomainDocument, options);
}
export type ValidateApprovedAccessDomainMutationHookResult = ReturnType<typeof useValidateApprovedAccessDomainMutation>;
export type ValidateApprovedAccessDomainMutationResult = Apollo.MutationResult<ValidateApprovedAccessDomainMutation>;
export type ValidateApprovedAccessDomainMutationOptions = Apollo.BaseMutationOptions<ValidateApprovedAccessDomainMutation, ValidateApprovedAccessDomainMutationVariables>;
export const GetApprovedAccessDomainsDocument = gql`
query GetApprovedAccessDomains {
getApprovedAccessDomains {
id
createdAt
domain
isValidated
}
}
`;
/**
* __useGetApprovedAccessDomainsQuery__
*
* To run a query within a React component, call `useGetApprovedAccessDomainsQuery` and pass it any options that fit your needs.
* When your component renders, `useGetApprovedAccessDomainsQuery` returns an object from Apollo Client that contains loading, error, and data properties
* you can use to render your UI.
*
* @param baseOptions options that will be passed into the query, supported options are listed on: https://www.apollographql.com/docs/react/api/react-hooks/#options;
*
* @example
* const { data, loading, error } = useGetApprovedAccessDomainsQuery({
* variables: {
* },
* });
*/
export function useGetApprovedAccessDomainsQuery(baseOptions?: Apollo.QueryHookOptions<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useQuery<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>(GetApprovedAccessDomainsDocument, options);
}
export function useGetApprovedAccessDomainsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>) {
const options = {...defaultOptions, ...baseOptions}
return Apollo.useLazyQuery<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>(GetApprovedAccessDomainsDocument, options);
}
export type GetApprovedAccessDomainsQueryHookResult = ReturnType<typeof useGetApprovedAccessDomainsQuery>;
export type GetApprovedAccessDomainsLazyQueryHookResult = ReturnType<typeof useGetApprovedAccessDomainsLazyQuery>;
export type GetApprovedAccessDomainsQueryResult = Apollo.QueryResult<GetApprovedAccessDomainsQuery, GetApprovedAccessDomainsQueryVariables>;
export const GetSsoIdentityProvidersDocument = gql`
query GetSSOIdentityProviders {
getSSOIdentityProviders {

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',

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)