mirror of
https://github.com/twentyhq/twenty
synced 2026-04-21 13:37:22 +00:00
Outbound message domains (#14557)
Co-authored-by: greptile-apps[bot] <165735046+greptile-apps[bot]@users.noreply.github.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com> Co-authored-by: Félix Malfait <felix@twenty.com>
This commit is contained in:
parent
6ea7aea92b
commit
a1ad8e3a1e
50 changed files with 3053 additions and 103 deletions
|
|
@ -488,6 +488,7 @@ export enum ConfigVariableType {
|
|||
|
||||
export enum ConfigVariablesGroup {
|
||||
AnalyticsConfig = 'AnalyticsConfig',
|
||||
AwsSesSettings = 'AwsSesSettings',
|
||||
BillingConfig = 'BillingConfig',
|
||||
CaptchaConfig = 'CaptchaConfig',
|
||||
CloudflareConfig = 'CloudflareConfig',
|
||||
|
|
@ -1034,6 +1035,29 @@ export type EmailPasswordResetLink = {
|
|||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type EmailingDomain = {
|
||||
__typename?: 'EmailingDomain';
|
||||
createdAt: Scalars['DateTime'];
|
||||
domain: Scalars['String'];
|
||||
driver: EmailingDomainDriver;
|
||||
id: Scalars['UUID'];
|
||||
status: EmailingDomainStatus;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
verificationRecords?: Maybe<Array<VerificationRecord>>;
|
||||
verifiedAt?: Maybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
export enum EmailingDomainDriver {
|
||||
AWS_SES = 'AWS_SES'
|
||||
}
|
||||
|
||||
export enum EmailingDomainStatus {
|
||||
FAILED = 'FAILED',
|
||||
PENDING = 'PENDING',
|
||||
TEMPORARY_FAILURE = 'TEMPORARY_FAILURE',
|
||||
VERIFIED = 'VERIFIED'
|
||||
}
|
||||
|
||||
export type ExecuteServerlessFunctionInput = {
|
||||
/** Id of the serverless function to execute */
|
||||
id: Scalars['UUID'];
|
||||
|
|
@ -1065,6 +1089,7 @@ export enum FeatureFlagKey {
|
|||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED',
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
|
|
@ -1476,6 +1501,7 @@ export type Mutation = {
|
|||
createCoreViewSort: CoreViewSort;
|
||||
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createEmailingDomain: EmailingDomain;
|
||||
createFile: File;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createObjectEvent: Analytics;
|
||||
|
|
@ -1504,6 +1530,7 @@ export type Mutation = {
|
|||
deleteCoreViewSort: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||
deleteEmailingDomain: Scalars['Boolean'];
|
||||
deleteFile: File;
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneField: Field;
|
||||
|
|
@ -1613,6 +1640,7 @@ export type Mutation = {
|
|||
upsertPermissionFlags: Array<PermissionFlag>;
|
||||
userLookupAdminPanel: UserLookup;
|
||||
validateApprovedAccessDomain: ApprovedAccessDomain;
|
||||
verifyEmailingDomain: EmailingDomain;
|
||||
verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput;
|
||||
};
|
||||
|
||||
|
|
@ -1725,6 +1753,12 @@ export type MutationCreateDraftFromWorkflowVersionArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationCreateEmailingDomainArgs = {
|
||||
domain: Scalars['String'];
|
||||
driver: EmailingDomainDriver;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateFileArgs = {
|
||||
file: Scalars['Upload'];
|
||||
};
|
||||
|
|
@ -1863,6 +1897,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationDeleteEmailingDomainArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteFileArgs = {
|
||||
fileId: Scalars['UUID'];
|
||||
};
|
||||
|
|
@ -2409,6 +2448,11 @@ export type MutationValidateApprovedAccessDomainArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationVerifyEmailingDomainArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs = {
|
||||
otp: Scalars['String'];
|
||||
};
|
||||
|
|
@ -2742,6 +2786,7 @@ export type Query = {
|
|||
getCoreViewSorts: Array<CoreViewSort>;
|
||||
getCoreViews: Array<CoreView>;
|
||||
getDatabaseConfigVariable: ConfigVariable;
|
||||
getEmailingDomains: Array<EmailingDomain>;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
getMeteredProductsUsage: Array<BillingMeteredProductUsageOutput>;
|
||||
getPageLayout?: Maybe<PageLayout>;
|
||||
|
|
@ -3899,6 +3944,14 @@ export type ValidatePasswordResetToken = {
|
|||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type VerificationRecord = {
|
||||
__typename?: 'VerificationRecord';
|
||||
key: Scalars['String'];
|
||||
priority?: Maybe<Scalars['Float']>;
|
||||
type: Scalars['String'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type VerifyTwoFactorAuthenticationMethodOutput = {
|
||||
__typename?: 'VerifyTwoFactorAuthenticationMethodOutput';
|
||||
success: Scalars['Boolean'];
|
||||
|
|
@ -4916,6 +4969,33 @@ export type FindManyPublicDomainsQueryVariables = Exact<{ [key: string]: never;
|
|||
|
||||
export type FindManyPublicDomainsQuery = { __typename?: 'Query', findManyPublicDomains: Array<{ __typename?: 'PublicDomain', id: string, domain: string, isValidated: boolean, createdAt: string }> };
|
||||
|
||||
export type CreateEmailingDomainMutationVariables = Exact<{
|
||||
domain: Scalars['String'];
|
||||
driver: EmailingDomainDriver;
|
||||
}>;
|
||||
|
||||
|
||||
export type CreateEmailingDomainMutation = { __typename?: 'Mutation', createEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null } };
|
||||
|
||||
export type DeleteEmailingDomainMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type DeleteEmailingDomainMutation = { __typename?: 'Mutation', deleteEmailingDomain: boolean };
|
||||
|
||||
export type VerifyEmailingDomainMutationVariables = Exact<{
|
||||
id: Scalars['String'];
|
||||
}>;
|
||||
|
||||
|
||||
export type VerifyEmailingDomainMutation = { __typename?: 'Mutation', verifyEmailingDomain: { __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string } };
|
||||
|
||||
export type GetEmailingDomainsQueryVariables = Exact<{ [key: string]: never; }>;
|
||||
|
||||
|
||||
export type GetEmailingDomainsQuery = { __typename?: 'Query', getEmailingDomains: Array<{ __typename?: 'EmailingDomain', id: string, domain: string, driver: EmailingDomainDriver, status: EmailingDomainStatus, verifiedAt?: string | null, createdAt: string, updatedAt: string, verificationRecords?: Array<{ __typename?: 'VerificationRecord', type: string, key: string, value: string, priority?: number | null }> | null }> };
|
||||
|
||||
export type UpdateLabPublicFeatureFlagMutationVariables = Exact<{
|
||||
input: UpdateLabPublicFeatureFlagInput;
|
||||
}>;
|
||||
|
|
@ -10015,6 +10095,168 @@ export function useFindManyPublicDomainsLazyQuery(baseOptions?: Apollo.LazyQuery
|
|||
export type FindManyPublicDomainsQueryHookResult = ReturnType<typeof useFindManyPublicDomainsQuery>;
|
||||
export type FindManyPublicDomainsLazyQueryHookResult = ReturnType<typeof useFindManyPublicDomainsLazyQuery>;
|
||||
export type FindManyPublicDomainsQueryResult = Apollo.QueryResult<FindManyPublicDomainsQuery, FindManyPublicDomainsQueryVariables>;
|
||||
export const CreateEmailingDomainDocument = gql`
|
||||
mutation CreateEmailingDomain($domain: String!, $driver: EmailingDomainDriver!) {
|
||||
createEmailingDomain(domain: $domain, driver: $driver) {
|
||||
id
|
||||
domain
|
||||
driver
|
||||
status
|
||||
verifiedAt
|
||||
verificationRecords {
|
||||
type
|
||||
key
|
||||
value
|
||||
priority
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type CreateEmailingDomainMutationFn = Apollo.MutationFunction<CreateEmailingDomainMutation, CreateEmailingDomainMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useCreateEmailingDomainMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useCreateEmailingDomainMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useCreateEmailingDomainMutation` 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 [createEmailingDomainMutation, { data, loading, error }] = useCreateEmailingDomainMutation({
|
||||
* variables: {
|
||||
* domain: // value for 'domain'
|
||||
* driver: // value for 'driver'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useCreateEmailingDomainMutation(baseOptions?: Apollo.MutationHookOptions<CreateEmailingDomainMutation, CreateEmailingDomainMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<CreateEmailingDomainMutation, CreateEmailingDomainMutationVariables>(CreateEmailingDomainDocument, options);
|
||||
}
|
||||
export type CreateEmailingDomainMutationHookResult = ReturnType<typeof useCreateEmailingDomainMutation>;
|
||||
export type CreateEmailingDomainMutationResult = Apollo.MutationResult<CreateEmailingDomainMutation>;
|
||||
export type CreateEmailingDomainMutationOptions = Apollo.BaseMutationOptions<CreateEmailingDomainMutation, CreateEmailingDomainMutationVariables>;
|
||||
export const DeleteEmailingDomainDocument = gql`
|
||||
mutation DeleteEmailingDomain($id: String!) {
|
||||
deleteEmailingDomain(id: $id)
|
||||
}
|
||||
`;
|
||||
export type DeleteEmailingDomainMutationFn = Apollo.MutationFunction<DeleteEmailingDomainMutation, DeleteEmailingDomainMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useDeleteEmailingDomainMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useDeleteEmailingDomainMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useDeleteEmailingDomainMutation` 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 [deleteEmailingDomainMutation, { data, loading, error }] = useDeleteEmailingDomainMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useDeleteEmailingDomainMutation(baseOptions?: Apollo.MutationHookOptions<DeleteEmailingDomainMutation, DeleteEmailingDomainMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<DeleteEmailingDomainMutation, DeleteEmailingDomainMutationVariables>(DeleteEmailingDomainDocument, options);
|
||||
}
|
||||
export type DeleteEmailingDomainMutationHookResult = ReturnType<typeof useDeleteEmailingDomainMutation>;
|
||||
export type DeleteEmailingDomainMutationResult = Apollo.MutationResult<DeleteEmailingDomainMutation>;
|
||||
export type DeleteEmailingDomainMutationOptions = Apollo.BaseMutationOptions<DeleteEmailingDomainMutation, DeleteEmailingDomainMutationVariables>;
|
||||
export const VerifyEmailingDomainDocument = gql`
|
||||
mutation VerifyEmailingDomain($id: String!) {
|
||||
verifyEmailingDomain(id: $id) {
|
||||
id
|
||||
domain
|
||||
driver
|
||||
status
|
||||
verifiedAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
export type VerifyEmailingDomainMutationFn = Apollo.MutationFunction<VerifyEmailingDomainMutation, VerifyEmailingDomainMutationVariables>;
|
||||
|
||||
/**
|
||||
* __useVerifyEmailingDomainMutation__
|
||||
*
|
||||
* To run a mutation, you first call `useVerifyEmailingDomainMutation` within a React component and pass it any options that fit your needs.
|
||||
* When your component renders, `useVerifyEmailingDomainMutation` 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 [verifyEmailingDomainMutation, { data, loading, error }] = useVerifyEmailingDomainMutation({
|
||||
* variables: {
|
||||
* id: // value for 'id'
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useVerifyEmailingDomainMutation(baseOptions?: Apollo.MutationHookOptions<VerifyEmailingDomainMutation, VerifyEmailingDomainMutationVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useMutation<VerifyEmailingDomainMutation, VerifyEmailingDomainMutationVariables>(VerifyEmailingDomainDocument, options);
|
||||
}
|
||||
export type VerifyEmailingDomainMutationHookResult = ReturnType<typeof useVerifyEmailingDomainMutation>;
|
||||
export type VerifyEmailingDomainMutationResult = Apollo.MutationResult<VerifyEmailingDomainMutation>;
|
||||
export type VerifyEmailingDomainMutationOptions = Apollo.BaseMutationOptions<VerifyEmailingDomainMutation, VerifyEmailingDomainMutationVariables>;
|
||||
export const GetEmailingDomainsDocument = gql`
|
||||
query GetEmailingDomains {
|
||||
getEmailingDomains {
|
||||
id
|
||||
domain
|
||||
driver
|
||||
status
|
||||
verifiedAt
|
||||
verificationRecords {
|
||||
type
|
||||
key
|
||||
value
|
||||
priority
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
||||
/**
|
||||
* __useGetEmailingDomainsQuery__
|
||||
*
|
||||
* To run a query within a React component, call `useGetEmailingDomainsQuery` and pass it any options that fit your needs.
|
||||
* When your component renders, `useGetEmailingDomainsQuery` 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 } = useGetEmailingDomainsQuery({
|
||||
* variables: {
|
||||
* },
|
||||
* });
|
||||
*/
|
||||
export function useGetEmailingDomainsQuery(baseOptions?: Apollo.QueryHookOptions<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useQuery<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>(GetEmailingDomainsDocument, options);
|
||||
}
|
||||
export function useGetEmailingDomainsLazyQuery(baseOptions?: Apollo.LazyQueryHookOptions<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>) {
|
||||
const options = {...defaultOptions, ...baseOptions}
|
||||
return Apollo.useLazyQuery<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>(GetEmailingDomainsDocument, options);
|
||||
}
|
||||
export type GetEmailingDomainsQueryHookResult = ReturnType<typeof useGetEmailingDomainsQuery>;
|
||||
export type GetEmailingDomainsLazyQueryHookResult = ReturnType<typeof useGetEmailingDomainsLazyQuery>;
|
||||
export type GetEmailingDomainsQueryResult = Apollo.QueryResult<GetEmailingDomainsQuery, GetEmailingDomainsQueryVariables>;
|
||||
export const UpdateLabPublicFeatureFlagDocument = gql`
|
||||
mutation UpdateLabPublicFeatureFlag($input: UpdateLabPublicFeatureFlagInput!) {
|
||||
updateLabPublicFeatureFlag(input: $input) {
|
||||
|
|
|
|||
|
|
@ -488,6 +488,7 @@ export enum ConfigVariableType {
|
|||
|
||||
export enum ConfigVariablesGroup {
|
||||
AnalyticsConfig = 'AnalyticsConfig',
|
||||
AwsSesSettings = 'AwsSesSettings',
|
||||
BillingConfig = 'BillingConfig',
|
||||
CaptchaConfig = 'CaptchaConfig',
|
||||
CloudflareConfig = 'CloudflareConfig',
|
||||
|
|
@ -998,6 +999,29 @@ export type EmailPasswordResetLink = {
|
|||
success: Scalars['Boolean'];
|
||||
};
|
||||
|
||||
export type EmailingDomain = {
|
||||
__typename?: 'EmailingDomain';
|
||||
createdAt: Scalars['DateTime'];
|
||||
domain: Scalars['String'];
|
||||
driver: EmailingDomainDriver;
|
||||
id: Scalars['UUID'];
|
||||
status: EmailingDomainStatus;
|
||||
updatedAt: Scalars['DateTime'];
|
||||
verificationRecords?: Maybe<Array<VerificationRecord>>;
|
||||
verifiedAt?: Maybe<Scalars['DateTime']>;
|
||||
};
|
||||
|
||||
export enum EmailingDomainDriver {
|
||||
AWS_SES = 'AWS_SES'
|
||||
}
|
||||
|
||||
export enum EmailingDomainStatus {
|
||||
FAILED = 'FAILED',
|
||||
PENDING = 'PENDING',
|
||||
TEMPORARY_FAILURE = 'TEMPORARY_FAILURE',
|
||||
VERIFIED = 'VERIFIED'
|
||||
}
|
||||
|
||||
export type ExecuteServerlessFunctionInput = {
|
||||
/** Id of the serverless function to execute */
|
||||
id: Scalars['UUID'];
|
||||
|
|
@ -1029,6 +1053,7 @@ export enum FeatureFlagKey {
|
|||
IS_CORE_VIEW_SYNCING_ENABLED = 'IS_CORE_VIEW_SYNCING_ENABLED',
|
||||
IS_DATABASE_EVENT_TRIGGER_ENABLED = 'IS_DATABASE_EVENT_TRIGGER_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED',
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED = 'IS_IMAP_SMTP_CALDAV_ENABLED',
|
||||
IS_JSON_FILTER_ENABLED = 'IS_JSON_FILTER_ENABLED',
|
||||
|
|
@ -1433,6 +1458,7 @@ export type Mutation = {
|
|||
createCoreViewSort: CoreViewSort;
|
||||
createDatabaseConfigVariable: Scalars['Boolean'];
|
||||
createDraftFromWorkflowVersion: WorkflowVersion;
|
||||
createEmailingDomain: EmailingDomain;
|
||||
createFile: File;
|
||||
createOIDCIdentityProvider: SetupSsoOutput;
|
||||
createObjectEvent: Analytics;
|
||||
|
|
@ -1460,6 +1486,7 @@ export type Mutation = {
|
|||
deleteCoreViewSort: Scalars['Boolean'];
|
||||
deleteCurrentWorkspace: Workspace;
|
||||
deleteDatabaseConfigVariable: Scalars['Boolean'];
|
||||
deleteEmailingDomain: Scalars['Boolean'];
|
||||
deleteFile: File;
|
||||
deleteOneAgent: Agent;
|
||||
deleteOneField: Field;
|
||||
|
|
@ -1564,6 +1591,7 @@ export type Mutation = {
|
|||
upsertPermissionFlags: Array<PermissionFlag>;
|
||||
userLookupAdminPanel: UserLookup;
|
||||
validateApprovedAccessDomain: ApprovedAccessDomain;
|
||||
verifyEmailingDomain: EmailingDomain;
|
||||
verifyTwoFactorAuthenticationMethodForAuthenticatedUser: VerifyTwoFactorAuthenticationMethodOutput;
|
||||
};
|
||||
|
||||
|
|
@ -1676,6 +1704,12 @@ export type MutationCreateDraftFromWorkflowVersionArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationCreateEmailingDomainArgs = {
|
||||
domain: Scalars['String'];
|
||||
driver: EmailingDomainDriver;
|
||||
};
|
||||
|
||||
|
||||
export type MutationCreateFileArgs = {
|
||||
file: Scalars['Upload'];
|
||||
};
|
||||
|
|
@ -1799,6 +1833,11 @@ export type MutationDeleteDatabaseConfigVariableArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationDeleteEmailingDomainArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationDeleteFileArgs = {
|
||||
fileId: Scalars['UUID'];
|
||||
};
|
||||
|
|
@ -2320,6 +2359,11 @@ export type MutationValidateApprovedAccessDomainArgs = {
|
|||
};
|
||||
|
||||
|
||||
export type MutationVerifyEmailingDomainArgs = {
|
||||
id: Scalars['String'];
|
||||
};
|
||||
|
||||
|
||||
export type MutationVerifyTwoFactorAuthenticationMethodForAuthenticatedUserArgs = {
|
||||
otp: Scalars['String'];
|
||||
};
|
||||
|
|
@ -2650,6 +2694,7 @@ export type Query = {
|
|||
getCoreViewSorts: Array<CoreViewSort>;
|
||||
getCoreViews: Array<CoreView>;
|
||||
getDatabaseConfigVariable: ConfigVariable;
|
||||
getEmailingDomains: Array<EmailingDomain>;
|
||||
getIndicatorHealthStatus: AdminPanelHealthServiceData;
|
||||
getMeteredProductsUsage: Array<BillingMeteredProductUsageOutput>;
|
||||
getPageLayout?: Maybe<PageLayout>;
|
||||
|
|
@ -3727,6 +3772,14 @@ export type ValidatePasswordResetToken = {
|
|||
id: Scalars['UUID'];
|
||||
};
|
||||
|
||||
export type VerificationRecord = {
|
||||
__typename?: 'VerificationRecord';
|
||||
key: Scalars['String'];
|
||||
priority?: Maybe<Scalars['Float']>;
|
||||
type: Scalars['String'];
|
||||
value: Scalars['String'];
|
||||
};
|
||||
|
||||
export type VerifyTwoFactorAuthenticationMethodOutput = {
|
||||
__typename?: 'VerifyTwoFactorAuthenticationMethodOutput';
|
||||
success: Scalars['Boolean'];
|
||||
|
|
|
|||
|
|
@ -3,9 +3,9 @@ import { Route, Routes } from 'react-router-dom';
|
|||
|
||||
import { SettingsProtectedRouteWrapper } from '@/settings/components/SettingsProtectedRouteWrapper';
|
||||
import { SettingsSkeletonLoader } from '@/settings/components/SettingsSkeletonLoader';
|
||||
import { SettingPublicDomain } from '@/settings/domains/components/SettingPublicDomain';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { PermissionFlagType } from '~/generated/graphql';
|
||||
import { SettingPublicDomain } from '@/settings/domains/components/SettingPublicDomain';
|
||||
|
||||
const SettingsGraphQLPlayground = lazy(() =>
|
||||
import(
|
||||
|
|
@ -310,6 +310,22 @@ const SettingsSecurityApprovedAccessDomain = lazy(() =>
|
|||
),
|
||||
);
|
||||
|
||||
const SettingsNewEmailingDomain = lazy(() =>
|
||||
import('~/pages/settings/emailing-domains/SettingsNewEmailingDomain').then(
|
||||
(module) => ({
|
||||
default: module.SettingsNewEmailingDomain,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsEmailingDomainDetail = lazy(() =>
|
||||
import('~/pages/settings/emailing-domains/SettingsEmailingDomainDetail').then(
|
||||
(module) => ({
|
||||
default: module.SettingsEmailingDomainDetail,
|
||||
}),
|
||||
),
|
||||
);
|
||||
|
||||
const SettingsAdmin = lazy(() =>
|
||||
import('~/pages/settings/admin-panel/SettingsAdmin').then((module) => ({
|
||||
default: module.SettingsAdmin,
|
||||
|
|
@ -429,6 +445,14 @@ export const SettingsRoutes = ({
|
|||
/>
|
||||
<Route path={SettingsPath.Billing} element={<SettingsBilling />} />
|
||||
<Route path={SettingsPath.Domain} element={<SettingsDomain />} />
|
||||
<Route
|
||||
path={SettingsPath.NewEmailingDomain}
|
||||
element={<SettingsNewEmailingDomain />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.EmailingDomainDetail}
|
||||
element={<SettingsEmailingDomainDetail />}
|
||||
/>
|
||||
<Route
|
||||
path={SettingsPath.PublicDomain}
|
||||
element={<SettingPublicDomain />}
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ import { isAnalyticsEnabledState } from '@/client-config/states/isAnalyticsEnabl
|
|||
import { isAttachmentPreviewEnabledState } from '@/client-config/states/isAttachmentPreviewEnabledState';
|
||||
import { isConfigVariablesInDbEnabledState } from '@/client-config/states/isConfigVariablesInDbEnabledState';
|
||||
import { isDeveloperDefaultSignInPrefilledState } from '@/client-config/states/isDeveloperDefaultSignInPrefilledState';
|
||||
import { isEmailingDomainsEnabledState } from '@/client-config/states/isEmailingDomainsEnabledState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
|
||||
import { isGoogleMessagingEnabledState } from '@/client-config/states/isGoogleMessagingEnabledState';
|
||||
|
|
@ -105,6 +106,9 @@ export const useClientConfig = (): UseClientConfigResult => {
|
|||
const setIsImapSmtpCaldavEnabled = useSetRecoilState(
|
||||
isImapSmtpCaldavEnabledState,
|
||||
);
|
||||
const setIsEmailingDomainsEnabled = useSetRecoilState(
|
||||
isEmailingDomainsEnabledState,
|
||||
);
|
||||
|
||||
const setAppVersion = useSetRecoilState(appVersionState);
|
||||
|
||||
|
|
@ -179,6 +183,7 @@ export const useClientConfig = (): UseClientConfigResult => {
|
|||
|
||||
setCalendarBookingPageId(clientConfig?.calendarBookingPageId ?? null);
|
||||
setIsImapSmtpCaldavEnabled(clientConfig?.isImapSmtpCaldavEnabled);
|
||||
setIsEmailingDomainsEnabled(clientConfig?.isEmailingDomainsEnabled);
|
||||
} catch (err) {
|
||||
const error =
|
||||
err instanceof Error ? err : new Error('Failed to fetch client config');
|
||||
|
|
@ -211,6 +216,7 @@ export const useClientConfig = (): UseClientConfigResult => {
|
|||
setIsEmailVerificationRequired,
|
||||
setIsImapSmtpCaldavEnabled,
|
||||
setIsMultiWorkspaceEnabled,
|
||||
setIsEmailingDomainsEnabled,
|
||||
setLabPublicFeatureFlags,
|
||||
setMicrosoftCalendarEnabled,
|
||||
setMicrosoftMessagingEnabled,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,6 @@
|
|||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const isEmailingDomainsEnabledState = createState<boolean>({
|
||||
key: 'isEmailingDomainsEnabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
|
|
@ -32,6 +32,7 @@ export type ClientConfig = {
|
|||
isMicrosoftMessagingEnabled: boolean;
|
||||
isMultiWorkspaceEnabled: boolean;
|
||||
isImapSmtpCaldavEnabled: boolean;
|
||||
isEmailingDomainsEnabled: boolean;
|
||||
publicFeatureFlags: Array<PublicFeatureFlag>;
|
||||
sentry: Sentry;
|
||||
signInPrefilled: boolean;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,122 @@
|
|||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { capitalize, isDefined } from 'twenty-shared/utils';
|
||||
import { OverflowingTextWithTooltip, Status } from 'twenty-ui/display';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
|
||||
type RecordStatus = {
|
||||
status: string;
|
||||
statusColor: ThemeColor;
|
||||
};
|
||||
|
||||
type DnsRecordBase = {
|
||||
type: string;
|
||||
key: string;
|
||||
value: string;
|
||||
priority?: number | null;
|
||||
ttl?: string;
|
||||
};
|
||||
|
||||
type DnsRecord = DnsRecordBase | (DnsRecordBase & RecordStatus);
|
||||
|
||||
type SettingsDnsRecordsTableProps = {
|
||||
records: DnsRecord[];
|
||||
};
|
||||
|
||||
const StyledTableRow = styled(TableRow)`
|
||||
& > * {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledTableCell = styled(TableCell)`
|
||||
font-family: monospace;
|
||||
`;
|
||||
|
||||
export const SettingsDnsRecordsTable = ({
|
||||
records,
|
||||
}: SettingsDnsRecordsTableProps) => {
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
if (!records || records.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const hasTtlRecords = records.some((record) => isDefined(record.ttl));
|
||||
const hasStatusRecords = records.some((record) => 'status' in record);
|
||||
const hasPriorityRecords = records.some((record) =>
|
||||
isDefined(record.priority),
|
||||
);
|
||||
|
||||
const buildGridColumns = () => {
|
||||
const baseColumns = ['max-content', '1fr', '1fr'];
|
||||
|
||||
if (hasPriorityRecords) baseColumns.push('max-content');
|
||||
if (hasTtlRecords) baseColumns.push('max-content');
|
||||
if (hasStatusRecords) baseColumns.push('max-content');
|
||||
|
||||
return baseColumns.join(' ');
|
||||
};
|
||||
|
||||
const gridAutoColumns = buildGridColumns();
|
||||
|
||||
return (
|
||||
<Table>
|
||||
<StyledTableRow gridAutoColumns={gridAutoColumns}>
|
||||
<TableHeader align="center">{t`Type`}</TableHeader>
|
||||
<TableHeader align="center">{t`Key`}</TableHeader>
|
||||
<TableHeader align="center">{t`Value`}</TableHeader>
|
||||
{hasPriorityRecords && (
|
||||
<TableHeader align="center">{t`Priority`}</TableHeader>
|
||||
)}
|
||||
{hasTtlRecords && <TableHeader align="center">{t`TTL`}</TableHeader>}
|
||||
{hasStatusRecords && (
|
||||
<TableHeader align="center">{t`Status`}</TableHeader>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
|
||||
{records.map((record) => (
|
||||
<StyledTableRow key={record.value} gridAutoColumns={gridAutoColumns}>
|
||||
<TableCell>{record.type}</TableCell>
|
||||
<StyledTableCell
|
||||
onClick={() => {
|
||||
copyToClipboard(record.key || '');
|
||||
}}
|
||||
>
|
||||
<OverflowingTextWithTooltip text={record.key} />
|
||||
</StyledTableCell>
|
||||
|
||||
<StyledTableCell
|
||||
onClick={() => {
|
||||
copyToClipboard(record.value);
|
||||
}}
|
||||
>
|
||||
<OverflowingTextWithTooltip text={record.value} />
|
||||
</StyledTableCell>
|
||||
|
||||
{hasPriorityRecords && (
|
||||
<StyledTableCell>{record.priority}</StyledTableCell>
|
||||
)}
|
||||
{hasTtlRecords && <StyledTableCell>{record.ttl}</StyledTableCell>}
|
||||
{hasStatusRecords && (
|
||||
<StyledTableCell>
|
||||
{'status' in record ? (
|
||||
<Status
|
||||
color={record.statusColor}
|
||||
text={capitalize(record.status)}
|
||||
/>
|
||||
) : null}
|
||||
</StyledTableCell>
|
||||
)}
|
||||
</StyledTableRow>
|
||||
))}
|
||||
</Table>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,84 +1,39 @@
|
|||
import { Table } from '@/ui/layout/table/components/Table';
|
||||
import { TableBody } from '@/ui/layout/table/components/TableBody';
|
||||
import { TableCell } from '@/ui/layout/table/components/TableCell';
|
||||
import { TableHeader } from '@/ui/layout/table/components/TableHeader';
|
||||
import { TableRow } from '@/ui/layout/table/components/TableRow';
|
||||
import styled from '@emotion/styled';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
import { useDebouncedCallback } from 'use-debounce';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
import {
|
||||
type DomainRecord,
|
||||
type DomainValidRecords,
|
||||
} from '~/generated/graphql';
|
||||
import { useCopyToClipboard } from '~/hooks/useCopyToClipboard';
|
||||
import { capitalize } from 'twenty-shared/utils';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { type ThemeColor } from 'twenty-ui/theme';
|
||||
import { Status } from 'twenty-ui/display';
|
||||
import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState';
|
||||
import { customDomainRecordsState } from '@/settings/domains/states/customDomainRecordsState';
|
||||
|
||||
const StyledTable = styled(Table)`
|
||||
border-bottom: 1px solid ${({ theme }) => theme.border.color.light};
|
||||
`;
|
||||
|
||||
const StyledTableCell = styled(TableCell)`
|
||||
overflow: hidden;
|
||||
display: block;
|
||||
padding: 0 ${({ theme }) => theme.spacing(3)} 0 0;
|
||||
|
||||
&:first-of-type {
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
&:last-of-type {
|
||||
padding-right: 0;
|
||||
}
|
||||
`;
|
||||
|
||||
const StyledButton = styled(Button)`
|
||||
border: 1px solid ${({ theme }) => theme.border.color.medium};
|
||||
color: ${({ theme }) => theme.font.color.tertiary};
|
||||
font-weight: ${({ theme }) => theme.font.weight.regular};
|
||||
height: ${({ theme }) => theme.spacing(6)};
|
||||
overflow: hidden;
|
||||
user-select: text;
|
||||
width: 100%;
|
||||
`;
|
||||
|
||||
export const SettingsDomainRecords = ({
|
||||
records,
|
||||
}: {
|
||||
records: DomainValidRecords['records'];
|
||||
}) => {
|
||||
const { customDomainRecords } = useRecoilValue(customDomainRecordsState);
|
||||
const currentWorkspace = useRecoilValue(currentWorkspaceState);
|
||||
|
||||
const { copyToClipboard } = useCopyToClipboard();
|
||||
|
||||
const copyToClipboardDebounced = useDebouncedCallback(
|
||||
(value: string) => copyToClipboard(value),
|
||||
200,
|
||||
);
|
||||
|
||||
const rowsDefinitions = [
|
||||
{ name: 'Domain Setup', validationType: 'redirection' as const },
|
||||
{ name: 'Secure Connection', validationType: 'ssl' as const },
|
||||
];
|
||||
|
||||
const defaultValues: { status: string; color: ThemeColor } =
|
||||
currentWorkspace?.customDomain === customDomainRecords?.domain
|
||||
const defaultValues: { status: string; statusColor: ThemeColor } =
|
||||
currentWorkspace?.customDomain
|
||||
? {
|
||||
status: 'success',
|
||||
color: 'green',
|
||||
statusColor: 'green',
|
||||
}
|
||||
: {
|
||||
status: 'loading',
|
||||
color: 'gray',
|
||||
statusColor: 'gray',
|
||||
};
|
||||
|
||||
const rows = rowsDefinitions.map<
|
||||
{ name: string; status: string; color: ThemeColor } & DomainRecord
|
||||
const transformedRecords = rowsDefinitions.map<
|
||||
{ statusColor: ThemeColor } & DomainRecord
|
||||
>((row) => {
|
||||
const record = records.find(
|
||||
({ validationType }) => validationType === row.validationType,
|
||||
|
|
@ -89,55 +44,23 @@ export const SettingsDomainRecords = ({
|
|||
}
|
||||
|
||||
return {
|
||||
name: row.name,
|
||||
color:
|
||||
statusColor:
|
||||
record && record.status === 'error'
|
||||
? 'red'
|
||||
: record && record.status === 'pending'
|
||||
? 'yellow'
|
||||
: defaultValues.color,
|
||||
: defaultValues.statusColor,
|
||||
...record,
|
||||
};
|
||||
});
|
||||
|
||||
return (
|
||||
<StyledTable>
|
||||
<TableRow gridAutoColumns="30% 16% 38% 16%">
|
||||
<TableHeader>Name</TableHeader>
|
||||
<TableHeader>Type</TableHeader>
|
||||
<TableHeader>Value</TableHeader>
|
||||
<TableHeader></TableHeader>
|
||||
</TableRow>
|
||||
<TableBody>
|
||||
{rows.map((row) => (
|
||||
<TableRow gridAutoColumns="30% 16% 38% 16%" key={row.name}>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={row.key}
|
||||
onClick={() => copyToClipboardDebounced(row.key)}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={row.type.toUpperCase()}
|
||||
onClick={() => copyToClipboardDebounced(row.type.toUpperCase())}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<StyledButton
|
||||
title={row.value}
|
||||
onClick={() => copyToClipboardDebounced(row.value)}
|
||||
type="button"
|
||||
/>
|
||||
</StyledTableCell>
|
||||
<StyledTableCell>
|
||||
<Status color={row.color} text={capitalize(row.status)} />
|
||||
</StyledTableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</TableBody>
|
||||
</StyledTable>
|
||||
<Section>
|
||||
<H2Title
|
||||
title="Domain Setup"
|
||||
description="Configure these DNS records with your domain provider"
|
||||
/>
|
||||
<SettingsDnsRecordsTable records={transformedRecords} />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -0,0 +1,78 @@
|
|||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { Dropdown } from '@/ui/layout/dropdown/components/Dropdown';
|
||||
import { DropdownContent } from '@/ui/layout/dropdown/components/DropdownContent';
|
||||
import { DropdownMenuItemsContainer } from '@/ui/layout/dropdown/components/DropdownMenuItemsContainer';
|
||||
import { useCloseDropdown } from '@/ui/layout/dropdown/hooks/useCloseDropdown';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { IconDotsVertical, IconTrash } from 'twenty-ui/display';
|
||||
import { LightIconButton } from 'twenty-ui/input';
|
||||
import { MenuItem } from 'twenty-ui/navigation';
|
||||
import {
|
||||
type GetEmailingDomainsQuery,
|
||||
useDeleteEmailingDomainMutation,
|
||||
useGetEmailingDomainsQuery,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsEmailingDomainRowDropdownMenuProps = {
|
||||
emailingDomain: GetEmailingDomainsQuery['getEmailingDomains'][0];
|
||||
};
|
||||
|
||||
export const SettingsEmailingDomainRowDropdownMenu = ({
|
||||
emailingDomain,
|
||||
}: SettingsEmailingDomainRowDropdownMenuProps) => {
|
||||
const dropdownId = `settings-emailing-domain-row-${emailingDomain.id}`;
|
||||
|
||||
const { enqueueErrorSnackBar, enqueueSuccessSnackBar } = useSnackBar();
|
||||
|
||||
const { closeDropdown } = useCloseDropdown();
|
||||
|
||||
const { refetch: refetchEmailingDomains } = useGetEmailingDomainsQuery();
|
||||
|
||||
const [deleteEmailingDomainMutation] = useDeleteEmailingDomainMutation();
|
||||
|
||||
const handleDeleteEmailingDomain = async () => {
|
||||
try {
|
||||
await deleteEmailingDomainMutation({
|
||||
variables: {
|
||||
id: emailingDomain.id,
|
||||
},
|
||||
});
|
||||
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Emailing domain deleted successfully`,
|
||||
});
|
||||
|
||||
await refetchEmailingDomains();
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
...(error instanceof ApolloError ? { apolloError: error } : {}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dropdown
|
||||
dropdownId={dropdownId}
|
||||
dropdownPlacement="right-start"
|
||||
clickableComponent={
|
||||
<LightIconButton Icon={IconDotsVertical} accent="tertiary" />
|
||||
}
|
||||
dropdownComponents={
|
||||
<DropdownContent>
|
||||
<DropdownMenuItemsContainer>
|
||||
<MenuItem
|
||||
accent="danger"
|
||||
LeftIcon={IconTrash}
|
||||
text={t`Delete`}
|
||||
onClick={() => {
|
||||
handleDeleteEmailingDomain();
|
||||
closeDropdown(dropdownId);
|
||||
}}
|
||||
/>
|
||||
</DropdownMenuItemsContainer>
|
||||
</DropdownContent>
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
import { SettingsDnsRecordsTable } from '@/settings/components/SettingsDnsRecordsTable';
|
||||
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { t } from '@lingui/core/macro';
|
||||
import { H2Title, IconRefresh } from 'twenty-ui/display';
|
||||
import { Button } from 'twenty-ui/input';
|
||||
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
type EmailingDomain,
|
||||
useVerifyEmailingDomainMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
|
||||
type SettingsEmailingDomainVerificationRecordsProps = {
|
||||
domain: EmailingDomain;
|
||||
};
|
||||
|
||||
export const SettingsEmailingDomainVerificationRecords = ({
|
||||
domain,
|
||||
}: SettingsEmailingDomainVerificationRecordsProps) => {
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const [verifyEmailingDomainMutation, { loading: isVerifying }] =
|
||||
useVerifyEmailingDomainMutation();
|
||||
|
||||
if (!domain.verificationRecords || domain.verificationRecords.length === 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const handleVerifyEmailingDomain = async () => {
|
||||
try {
|
||||
await verifyEmailingDomainMutation({
|
||||
variables: {
|
||||
id: domain.id,
|
||||
},
|
||||
});
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Started verification process`,
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
...(error instanceof ApolloError ? { apolloError: error } : {}),
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`DNS Records`}
|
||||
description={t`Add these records to verify your domain.`}
|
||||
adornment={
|
||||
<Button
|
||||
onClick={handleVerifyEmailingDomain}
|
||||
isLoading={isVerifying}
|
||||
variant="secondary"
|
||||
Icon={IconRefresh}
|
||||
size="small"
|
||||
title={t`Check verification`}
|
||||
disabled={isVerifying}
|
||||
/>
|
||||
}
|
||||
/>
|
||||
<SettingsDnsRecordsTable records={domain.verificationRecords} />
|
||||
</Section>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const CREATE_EMAILING_DOMAIN = gql`
|
||||
mutation CreateEmailingDomain(
|
||||
$domain: String!
|
||||
$driver: EmailingDomainDriver!
|
||||
) {
|
||||
createEmailingDomain(domain: $domain, driver: $driver) {
|
||||
id
|
||||
domain
|
||||
driver
|
||||
status
|
||||
verifiedAt
|
||||
verificationRecords {
|
||||
type
|
||||
key
|
||||
value
|
||||
priority
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const DELETE_EMAILING_DOMAIN = gql`
|
||||
mutation DeleteEmailingDomain($id: String!) {
|
||||
deleteEmailingDomain(id: $id)
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const VERIFY_EMAILING_DOMAIN = gql`
|
||||
mutation VerifyEmailingDomain($id: String!) {
|
||||
verifyEmailingDomain(id: $id) {
|
||||
id
|
||||
domain
|
||||
driver
|
||||
status
|
||||
verifiedAt
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -0,0 +1,21 @@
|
|||
import { gql } from '@apollo/client';
|
||||
|
||||
export const GET_ALL_EMAILING_DOMAINS = gql`
|
||||
query GetEmailingDomains {
|
||||
getEmailingDomains {
|
||||
id
|
||||
domain
|
||||
driver
|
||||
status
|
||||
verifiedAt
|
||||
verificationRecords {
|
||||
type
|
||||
key
|
||||
value
|
||||
priority
|
||||
}
|
||||
createdAt
|
||||
updatedAt
|
||||
}
|
||||
}
|
||||
`;
|
||||
|
|
@ -2,16 +2,18 @@ import styled from '@emotion/styled';
|
|||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { SettingsPublicDomainsListCard } from '@/settings/domains/components/SettingsPublicDomainsListCard';
|
||||
import { SettingsWorkspaceDomainCard } from '@/settings/domains/components/SettingsWorkspaceDomainCard';
|
||||
import { SettingsApprovedAccessDomainsListCard } from '@/settings/security/components/approvedAccessDomains/SettingsApprovedAccessDomainsListCard';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { useIsFeatureEnabled } from '@/workspace/hooks/useIsFeatureEnabled';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import { SettingsPublicDomainsListCard } from '@/settings/domains/components/SettingsPublicDomainsListCard';
|
||||
import { useFeatureFlagsMap } from '@/workspace/hooks/useFeatureFlagsMap';
|
||||
import { FeatureFlagKey } from '~/generated/graphql';
|
||||
import { SettingsEmailingDomains } from '~/pages/settings/emailing-domains/SettingsEmailingDomains';
|
||||
|
||||
const StyledMainContent = styled.div`
|
||||
display: flex;
|
||||
|
|
@ -31,6 +33,10 @@ export const SettingsDomains = () => {
|
|||
const isPublicDomainEnabled =
|
||||
featureFlags[FeatureFlagKey.IS_PUBLIC_DOMAIN_ENABLED];
|
||||
|
||||
const isEmailingDomainsEnabled = useIsFeatureEnabled(
|
||||
FeatureFlagKey.IS_EMAILING_DOMAIN_ENABLED,
|
||||
);
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`Domains`}
|
||||
|
|
@ -58,6 +64,15 @@ export const SettingsDomains = () => {
|
|||
/>
|
||||
<SettingsApprovedAccessDomainsListCard />
|
||||
</StyledSection>
|
||||
{isEmailingDomainsEnabled && (
|
||||
<StyledSection>
|
||||
<H2Title
|
||||
title={t`Emailing Domains`}
|
||||
description={t`Configure and verify domains for emailing from this workspace.`}
|
||||
/>
|
||||
<SettingsEmailingDomains />
|
||||
</StyledSection>
|
||||
)}
|
||||
{isPublicDomainEnabled && (
|
||||
<StyledSection>
|
||||
<H2Title
|
||||
|
|
|
|||
|
|
@ -0,0 +1,60 @@
|
|||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
|
||||
import { SettingsEmailingDomainVerificationRecords } from '@/settings/emailing-domains/components/SettingsEmailingDomainVerificationRecords';
|
||||
import { GET_ALL_EMAILING_DOMAINS } from '@/settings/emailing-domains/graphql/queries/getAllEmailingDomains';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { useQuery } from '@apollo/client';
|
||||
import { Trans } from '@lingui/react/macro';
|
||||
import { useParams } from 'react-router-dom';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { type GetEmailingDomainsQuery } from '~/generated-metadata/graphql';
|
||||
|
||||
export const SettingsEmailingDomainDetail = () => {
|
||||
const { domainId } = useParams<{ domainId: string }>();
|
||||
|
||||
const { data, loading, error } = useQuery<GetEmailingDomainsQuery>(
|
||||
GET_ALL_EMAILING_DOMAINS,
|
||||
{
|
||||
skip: !domainId,
|
||||
},
|
||||
);
|
||||
|
||||
const emailingDomain = data?.getEmailingDomains?.find(
|
||||
(domain) => domain.id === domainId,
|
||||
);
|
||||
|
||||
if (loading) {
|
||||
return <div>Loading...</div>;
|
||||
}
|
||||
|
||||
if (isDefined(error) || !isDefined(emailingDomain)) {
|
||||
return <Trans>Domain not found</Trans>;
|
||||
}
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={emailingDomain.domain}
|
||||
links={[
|
||||
{
|
||||
children: <Trans>Workspace</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: <Trans>Emailing Domains</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Domains),
|
||||
},
|
||||
{ children: emailingDomain.domain },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
{emailingDomain.verificationRecords &&
|
||||
emailingDomain.verificationRecords.length > 0 && (
|
||||
<SettingsEmailingDomainVerificationRecords
|
||||
domain={emailingDomain}
|
||||
/>
|
||||
)}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,77 @@
|
|||
import { Link, useNavigate } from 'react-router-dom';
|
||||
|
||||
import { SettingsCard } from '@/settings/components/SettingsCard';
|
||||
import { SettingsListCard } from '@/settings/components/SettingsListCard';
|
||||
|
||||
import { SettingsEmailingDomainRowDropdownMenu } from '@/settings/emailing-domains/components/SettingsEmailingDomainRowDropdownMenu';
|
||||
|
||||
import styled from '@emotion/styled';
|
||||
import { useLingui } from '@lingui/react/macro';
|
||||
import { useRecoilValue } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath } from 'twenty-shared/utils';
|
||||
import { IconMail, Status } from 'twenty-ui/display';
|
||||
import { useGetEmailingDomainsQuery } from '~/generated-metadata/graphql';
|
||||
import { dateLocaleState } from '~/localization/states/dateLocaleState';
|
||||
import { getColorByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusColor';
|
||||
import { getTextByEmailingDomainStatus } from '~/pages/settings/emailing-domains/utils/getEmailingDomainStatusText';
|
||||
import { beautifyPastDateRelativeToNow } from '~/utils/date-utils';
|
||||
|
||||
const StyledLink = styled(Link)`
|
||||
text-decoration: none;
|
||||
`;
|
||||
|
||||
export const SettingsEmailingDomains = () => {
|
||||
const { t } = useLingui();
|
||||
const { localeCatalog } = useRecoilValue(dateLocaleState);
|
||||
const navigate = useNavigate();
|
||||
|
||||
const { data, loading: isLoading } = useGetEmailingDomainsQuery();
|
||||
const emailingDomains = data?.getEmailingDomains ?? [];
|
||||
|
||||
const getItemDescription = (createdAt: string) => {
|
||||
const beautifyPastDateRelative = beautifyPastDateRelativeToNow(
|
||||
createdAt,
|
||||
localeCatalog,
|
||||
);
|
||||
return t`Added ${beautifyPastDateRelative}`;
|
||||
};
|
||||
|
||||
return isLoading || !emailingDomains.length ? (
|
||||
<StyledLink to={getSettingsPath(SettingsPath.NewEmailingDomain)}>
|
||||
<SettingsCard title={t`Add Emailing Domain`} Icon={<IconMail />} />
|
||||
</StyledLink>
|
||||
) : (
|
||||
<>
|
||||
<SettingsListCard
|
||||
items={emailingDomains}
|
||||
getItemLabel={({ domain }) => domain ?? ''}
|
||||
getItemDescription={({ createdAt }) => getItemDescription(createdAt)}
|
||||
RowIcon={IconMail}
|
||||
onRowClick={(emailingDomain) => {
|
||||
navigate(
|
||||
getSettingsPath(SettingsPath.EmailingDomainDetail, {
|
||||
domainId: emailingDomain.id,
|
||||
}),
|
||||
);
|
||||
}}
|
||||
RowRightComponent={({ item: emailingDomain }) => (
|
||||
<>
|
||||
<Status
|
||||
color={getColorByEmailingDomainStatus(emailingDomain.status)}
|
||||
text={getTextByEmailingDomainStatus(emailingDomain.status)}
|
||||
/>
|
||||
<SettingsEmailingDomainRowDropdownMenu
|
||||
emailingDomain={emailingDomain}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
hasFooter
|
||||
footerButtonLabel="Add Emailing Domain"
|
||||
onFooterButtonClick={() =>
|
||||
navigate(getSettingsPath(SettingsPath.NewEmailingDomain))
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,157 @@
|
|||
import { SaveAndCancelButtons } from '@/settings/components/SaveAndCancelButtons/SaveAndCancelButtons';
|
||||
import { SettingsPageContainer } from '@/settings/components/SettingsPageContainer';
|
||||
import { useSnackBar } from '@/ui/feedback/snack-bar-manager/hooks/useSnackBar';
|
||||
import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput';
|
||||
import { SubMenuTopBarContainer } from '@/ui/layout/page/components/SubMenuTopBarContainer';
|
||||
import { ApolloError } from '@apollo/client';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { useState } from 'react';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { H2Title } from 'twenty-ui/display';
|
||||
import { Section } from 'twenty-ui/layout';
|
||||
import {
|
||||
EmailingDomainDriver,
|
||||
useCreateEmailingDomainMutation,
|
||||
} from '~/generated-metadata/graphql';
|
||||
import { useNavigateSettings } from '~/hooks/useNavigateSettings';
|
||||
import {
|
||||
settingsEmailingDomainFormSchema,
|
||||
type SettingsEmailingDomainFormValues,
|
||||
} from '~/pages/settings/emailing-domains/validation-schemas/settingsEmailingDomainFormSchema';
|
||||
|
||||
type FieldErrors = Partial<
|
||||
Record<keyof SettingsEmailingDomainFormValues, string>
|
||||
>;
|
||||
export const SettingsNewEmailingDomain = () => {
|
||||
const navigate = useNavigateSettings();
|
||||
const { t } = useLingui();
|
||||
const { enqueueSuccessSnackBar, enqueueErrorSnackBar } = useSnackBar();
|
||||
|
||||
const [formValues, setFormValues] =
|
||||
useState<SettingsEmailingDomainFormValues>({
|
||||
driver: EmailingDomainDriver.AWS_SES,
|
||||
domain: '',
|
||||
});
|
||||
|
||||
const [fieldErrors, setFieldErrors] = useState<FieldErrors>({});
|
||||
const [isSubmitting, setIsSubmitting] = useState(false);
|
||||
|
||||
const [createEmailingDomain] = useCreateEmailingDomainMutation();
|
||||
|
||||
const validateForm = (): boolean => {
|
||||
const result = settingsEmailingDomainFormSchema.safeParse(formValues);
|
||||
|
||||
if (!result.success) {
|
||||
setFieldErrors(result.error?.flatten().fieldErrors as FieldErrors);
|
||||
return false;
|
||||
}
|
||||
|
||||
setFieldErrors({});
|
||||
return true;
|
||||
};
|
||||
|
||||
const handleFieldChange = (
|
||||
field: keyof SettingsEmailingDomainFormValues,
|
||||
value: string | EmailingDomainDriver,
|
||||
) => {
|
||||
setFormValues((prev) => ({
|
||||
...prev,
|
||||
[field]: value,
|
||||
}));
|
||||
|
||||
if (isDefined(fieldErrors[field])) {
|
||||
setFieldErrors((prev) => ({
|
||||
...prev,
|
||||
[field]: undefined,
|
||||
}));
|
||||
}
|
||||
};
|
||||
|
||||
const canSave = !isSubmitting;
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!validateForm()) {
|
||||
return;
|
||||
}
|
||||
|
||||
setIsSubmitting(true);
|
||||
|
||||
try {
|
||||
await createEmailingDomain({
|
||||
variables: {
|
||||
domain: formValues.domain,
|
||||
driver: formValues.driver,
|
||||
},
|
||||
onCompleted: (data) => {
|
||||
enqueueSuccessSnackBar({
|
||||
message: t`Emailing domain created successfully. Please verify the domain to start using it.`,
|
||||
});
|
||||
if (!data.createEmailingDomain?.id) return;
|
||||
|
||||
navigate(SettingsPath.EmailingDomainDetail, {
|
||||
domainId: data.createEmailingDomain.id,
|
||||
});
|
||||
},
|
||||
onError: (error) => {
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error instanceof ApolloError ? error : undefined,
|
||||
});
|
||||
},
|
||||
});
|
||||
} catch (error) {
|
||||
enqueueErrorSnackBar({
|
||||
apolloError: error instanceof ApolloError ? error : undefined,
|
||||
});
|
||||
} finally {
|
||||
setIsSubmitting(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<SubMenuTopBarContainer
|
||||
title={t`New Emailing Domain`}
|
||||
actionButton={
|
||||
<SaveAndCancelButtons
|
||||
onCancel={() => navigate(SettingsPath.Domains)}
|
||||
onSave={handleSave}
|
||||
isSaveDisabled={!canSave}
|
||||
/>
|
||||
}
|
||||
links={[
|
||||
{
|
||||
children: <Trans>Workspace</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Workspace),
|
||||
},
|
||||
{
|
||||
children: <Trans>Domains</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Domains),
|
||||
},
|
||||
{
|
||||
children: <Trans>Emailing Domains</Trans>,
|
||||
href: getSettingsPath(SettingsPath.Domains),
|
||||
},
|
||||
{ children: <Trans>New Emailing Domain</Trans> },
|
||||
]}
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<Section>
|
||||
<H2Title
|
||||
title={t`Domain`}
|
||||
description={t`The domain name you want to use for emailing`}
|
||||
/>
|
||||
<SettingsTextInput
|
||||
instanceId="emailing-domain"
|
||||
autoFocus
|
||||
autoComplete="off"
|
||||
value={formValues.domain}
|
||||
onChange={(value) => handleFieldChange('domain', value)}
|
||||
fullWidth
|
||||
placeholder="yourdomain.com"
|
||||
error={fieldErrors.domain}
|
||||
/>
|
||||
</Section>
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
);
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { EmailingDomainStatus } from '~/generated/graphql';
|
||||
|
||||
export const getColorByEmailingDomainStatus = (
|
||||
status: EmailingDomainStatus,
|
||||
) => {
|
||||
switch (status) {
|
||||
case EmailingDomainStatus.VERIFIED:
|
||||
return 'turquoise';
|
||||
case EmailingDomainStatus.PENDING:
|
||||
return 'orange';
|
||||
case EmailingDomainStatus.TEMPORARY_FAILURE:
|
||||
return 'orange';
|
||||
case EmailingDomainStatus.FAILED:
|
||||
return 'red';
|
||||
default:
|
||||
return 'gray';
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
import { t } from '@lingui/core/macro';
|
||||
import { EmailingDomainStatus } from '~/generated/graphql';
|
||||
|
||||
export const getTextByEmailingDomainStatus = (status: EmailingDomainStatus) => {
|
||||
switch (status) {
|
||||
case EmailingDomainStatus.VERIFIED:
|
||||
return t`Verified`;
|
||||
case EmailingDomainStatus.PENDING:
|
||||
return t`Pending`;
|
||||
case EmailingDomainStatus.TEMPORARY_FAILURE:
|
||||
return t`Temporary Failure`;
|
||||
case EmailingDomainStatus.FAILED:
|
||||
return t`Failed`;
|
||||
default:
|
||||
return t`Unknown`;
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,18 @@
|
|||
import { z } from 'zod';
|
||||
import { EmailingDomainDriver } from '~/generated-metadata/graphql';
|
||||
|
||||
export const settingsEmailingDomainFormSchema = z.object({
|
||||
domain: z
|
||||
.string()
|
||||
.min(1, 'Domain is required')
|
||||
.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])\.[a-zA-Z]{2,}$/,
|
||||
'Invalid domain format. Please enter a valid domain name.',
|
||||
)
|
||||
.max(256, 'Domain must be less than 256 characters.'),
|
||||
driver: z.nativeEnum(EmailingDomainDriver),
|
||||
});
|
||||
|
||||
export type SettingsEmailingDomainFormValues = z.infer<
|
||||
typeof settingsEmailingDomainFormSchema
|
||||
>;
|
||||
|
|
@ -56,4 +56,5 @@ export const mockedClientConfig: ClientConfig = {
|
|||
isConfigVariablesInDbEnabled: false,
|
||||
isImapSmtpCaldavEnabled: false,
|
||||
isTwoFactorAuthenticationEnabled: false,
|
||||
isEmailingDomainsEnabled: false,
|
||||
};
|
||||
|
|
|
|||
|
|
@ -21,6 +21,7 @@
|
|||
"@ai-sdk/xai": "^2.0.19",
|
||||
"@aws-sdk/client-lambda": "3.825.0",
|
||||
"@aws-sdk/client-s3": "3.825.0",
|
||||
"@aws-sdk/client-sesv2": "^3.888.0",
|
||||
"@aws-sdk/client-sts": "3.825.0",
|
||||
"@aws-sdk/credential-providers": "3.825.0",
|
||||
"@babel/preset-env": "7.26.9",
|
||||
|
|
|
|||
|
|
@ -0,0 +1,31 @@
|
|||
import { type MigrationInterface, type QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateEmailingDomainEntity1758388517321
|
||||
implements MigrationInterface
|
||||
{
|
||||
name = 'CreateEmailingDomainEntity1758388517321';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."emailingDomain_driver_enum" AS ENUM('AWS_SES')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TYPE "core"."emailingDomain_status_enum" AS ENUM('PENDING', 'VERIFIED', 'FAILED', 'TEMPORARY_FAILURE')`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "core"."emailingDomain" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "createdAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "updatedAt" TIMESTAMP WITH TIME ZONE NOT NULL DEFAULT now(), "domain" character varying NOT NULL, "driver" "core"."emailingDomain_driver_enum" NOT NULL, "status" "core"."emailingDomain_status_enum" NOT NULL DEFAULT 'PENDING', "verificationRecords" jsonb, "verifiedAt" TIMESTAMP WITH TIME ZONE, "workspaceId" uuid NOT NULL, CONSTRAINT "IDX_EMAILING_DOMAIN_DOMAIN_WORKSPACE_ID_UNIQUE" UNIQUE ("domain", "workspaceId"), CONSTRAINT "PK_dca7032537b5d307f8cc6d74f1d" PRIMARY KEY ("id"))`,
|
||||
);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."emailingDomain" ADD CONSTRAINT "FK_793a938bef2aae0a2129f78951f" FOREIGN KEY ("workspaceId") REFERENCES "core"."workspace"("id") ON DELETE CASCADE ON UPDATE NO ACTION`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "core"."emailingDomain" DROP CONSTRAINT "FK_793a938bef2aae0a2129f78951f"`,
|
||||
);
|
||||
await queryRunner.query(`DROP TABLE "core"."emailingDomain"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."emailingDomain_status_enum"`);
|
||||
await queryRunner.query(`DROP TYPE "core"."emailingDomain_driver_enum"`);
|
||||
}
|
||||
}
|
||||
|
|
@ -20,6 +20,7 @@ import { captchaModuleFactory } from 'src/engine/core-modules/captcha/captcha.mo
|
|||
import { CloudflareModule } from 'src/engine/core-modules/cloudflare/cloudflare.module';
|
||||
import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module';
|
||||
import { EmailModule } from 'src/engine/core-modules/email/email.module';
|
||||
import { EmailingDomainModule } from 'src/engine/core-modules/emailing-domain/emailing-domain.module';
|
||||
import { ExceptionHandlerModule } from 'src/engine/core-modules/exception-handler/exception-handler.module';
|
||||
import { exceptionHandlerModuleFactory } from 'src/engine/core-modules/exception-handler/exception-handler.module-factory';
|
||||
import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module';
|
||||
|
|
@ -83,6 +84,7 @@ import { FileModule } from './file/file.module';
|
|||
WorkspaceInvitationModule,
|
||||
WorkspaceSSOModule,
|
||||
ApprovedAccessDomainModule,
|
||||
EmailingDomainModule,
|
||||
PublicDomainModule,
|
||||
CloudflareModule,
|
||||
DnsManagerModule,
|
||||
|
|
|
|||
|
|
@ -0,0 +1,43 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
SESv2Client as SESClient,
|
||||
type SESv2ClientConfig as SESClientConfig,
|
||||
} from '@aws-sdk/client-sesv2';
|
||||
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class AwsSesClientProvider {
|
||||
private sesClient: SESClient | null = null;
|
||||
|
||||
constructor(private readonly twentyConfigService: TwentyConfigService) {}
|
||||
|
||||
public getSESClient(): SESClient {
|
||||
if (!this.sesClient) {
|
||||
const config: SESClientConfig = {
|
||||
region: this.twentyConfigService.get('AWS_SES_REGION'),
|
||||
};
|
||||
|
||||
const accessKeyId = this.twentyConfigService.get('AWS_SES_ACCESS_KEY_ID');
|
||||
const secretAccessKey = this.twentyConfigService.get(
|
||||
'AWS_SES_SECRET_ACCESS_KEY',
|
||||
);
|
||||
const sessionToken = this.twentyConfigService.get(
|
||||
'AWS_SES_SESSION_TOKEN',
|
||||
);
|
||||
|
||||
if (accessKeyId && secretAccessKey && sessionToken) {
|
||||
config.credentials = {
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
};
|
||||
}
|
||||
|
||||
this.sesClient = new SESClient(config);
|
||||
}
|
||||
|
||||
return this.sesClient;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,263 @@
|
|||
import { Logger } from '@nestjs/common';
|
||||
|
||||
import {
|
||||
CreateEmailIdentityCommand,
|
||||
CreateTenantCommand,
|
||||
CreateTenantResourceAssociationCommand,
|
||||
GetEmailIdentityCommand,
|
||||
PutEmailIdentityDkimAttributesCommand,
|
||||
} from '@aws-sdk/client-sesv2';
|
||||
|
||||
import { type AwsSesDriverConfig } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface';
|
||||
import {
|
||||
type DomainStatusInput,
|
||||
type DomainVerificationInput,
|
||||
type EmailingDomainDriverInterface,
|
||||
type EmailingDomainVerificationResult,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface';
|
||||
|
||||
import { type AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider';
|
||||
import { type AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
|
||||
import { EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { type VerificationRecord } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
|
||||
|
||||
export class AwsSesDriver implements EmailingDomainDriverInterface {
|
||||
private readonly logger = new Logger(AwsSesDriver.name);
|
||||
|
||||
constructor(
|
||||
private readonly config: AwsSesDriverConfig,
|
||||
private readonly awsSesClientProvider: AwsSesClientProvider,
|
||||
private readonly awsSesHandleErrorService: AwsSesHandleErrorService,
|
||||
) {}
|
||||
|
||||
async verifyDomain(
|
||||
input: DomainVerificationInput,
|
||||
): Promise<EmailingDomainVerificationResult> {
|
||||
try {
|
||||
this.logger.log(`Starting domain verification for: ${input.domain}`);
|
||||
|
||||
const tenantName = this.generateTenantName(input.workspaceId);
|
||||
|
||||
await this.ensureTenantExists(tenantName);
|
||||
|
||||
const { isVerified, verificationRecords } =
|
||||
await this.createOrUpdateEmailIdentity(input.domain, tenantName);
|
||||
|
||||
if (isVerified) {
|
||||
await this.enableDkimSigning(input.domain);
|
||||
}
|
||||
|
||||
return {
|
||||
status: isVerified
|
||||
? EmailingDomainStatus.VERIFIED
|
||||
: EmailingDomainStatus.PENDING,
|
||||
verifiedAt: isVerified ? new Date() : null,
|
||||
verificationRecords,
|
||||
};
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to verify domain ${input.domain}: ${error}`);
|
||||
this.awsSesHandleErrorService.handleAwsSesError(error, 'verifyDomain');
|
||||
}
|
||||
}
|
||||
|
||||
async getDomainStatus(
|
||||
input: DomainStatusInput,
|
||||
): Promise<EmailingDomainVerificationResult> {
|
||||
try {
|
||||
this.logger.log(`Getting domain status for: ${input.domain}`);
|
||||
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
|
||||
const getIdentityCommand = new GetEmailIdentityCommand({
|
||||
EmailIdentity: input.domain,
|
||||
});
|
||||
|
||||
const identityResponse = await sesClient.send(getIdentityCommand);
|
||||
|
||||
const status = this.determineVerificationStatus(identityResponse);
|
||||
const isFullyVerified = status === EmailingDomainStatus.VERIFIED;
|
||||
const verificationRecords = this.buildVerificationRecords(
|
||||
input.domain,
|
||||
identityResponse.DkimAttributes?.Tokens || [],
|
||||
);
|
||||
|
||||
return {
|
||||
status,
|
||||
verifiedAt: isFullyVerified ? new Date() : null,
|
||||
verificationRecords,
|
||||
};
|
||||
} catch (error) {
|
||||
if (error.name === 'NotFoundException') {
|
||||
return {
|
||||
status: EmailingDomainStatus.FAILED,
|
||||
verifiedAt: null,
|
||||
verificationRecords: [],
|
||||
};
|
||||
}
|
||||
|
||||
this.logger.error(
|
||||
`Failed to get domain status ${input.domain}: ${error}`,
|
||||
);
|
||||
this.awsSesHandleErrorService.handleAwsSesError(error, 'getDomainStatus');
|
||||
}
|
||||
}
|
||||
|
||||
private generateTenantName(workspaceId: string): string {
|
||||
return `twenty-workspace-${workspaceId}`;
|
||||
}
|
||||
|
||||
private async ensureTenantExists(tenantName: string): Promise<void> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
|
||||
try {
|
||||
await sesClient.send(new CreateTenantCommand({ TenantName: tenantName }));
|
||||
this.logger.log(`Created tenant: ${tenantName}`);
|
||||
} catch (error) {
|
||||
if (error.name === 'AlreadyExistsException') {
|
||||
this.logger.log(`Tenant already exists: ${tenantName}`);
|
||||
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createOrUpdateEmailIdentity(
|
||||
domain: string,
|
||||
tenantName: string,
|
||||
): Promise<{
|
||||
isVerified: boolean;
|
||||
verificationRecords: VerificationRecord[];
|
||||
}> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
|
||||
try {
|
||||
const getIdentityCommand = new GetEmailIdentityCommand({
|
||||
EmailIdentity: domain,
|
||||
});
|
||||
const existingIdentity = await sesClient.send(getIdentityCommand);
|
||||
|
||||
const isVerified = existingIdentity.VerifiedForSendingStatus === true;
|
||||
const verificationRecords = this.buildVerificationRecords(
|
||||
domain,
|
||||
existingIdentity.DkimAttributes?.Tokens || [],
|
||||
);
|
||||
|
||||
if (!isVerified) {
|
||||
await this.associateResourceWithTenant(domain, tenantName);
|
||||
}
|
||||
|
||||
return { isVerified, verificationRecords };
|
||||
} catch (error) {
|
||||
if (error.name === 'NotFoundException') {
|
||||
return await this.createNewEmailIdentity(domain, tenantName);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async createNewEmailIdentity(
|
||||
domain: string,
|
||||
tenantName: string,
|
||||
): Promise<{
|
||||
isVerified: boolean;
|
||||
verificationRecords: VerificationRecord[];
|
||||
}> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
|
||||
const createCommand = new CreateEmailIdentityCommand({
|
||||
EmailIdentity: domain,
|
||||
Tags: [{ Key: 'Tenant', Value: tenantName }],
|
||||
});
|
||||
|
||||
const createResponse = await sesClient.send(createCommand);
|
||||
const dkimTokens = createResponse.DkimAttributes?.Tokens || [];
|
||||
|
||||
await this.associateResourceWithTenant(domain, tenantName);
|
||||
|
||||
const verificationRecords = this.buildVerificationRecords(
|
||||
domain,
|
||||
dkimTokens,
|
||||
);
|
||||
|
||||
return {
|
||||
isVerified: false,
|
||||
verificationRecords,
|
||||
};
|
||||
}
|
||||
|
||||
private async associateResourceWithTenant(
|
||||
domain: string,
|
||||
tenantName: string,
|
||||
): Promise<void> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
|
||||
try {
|
||||
await sesClient.send(
|
||||
new CreateTenantResourceAssociationCommand({
|
||||
TenantName: tenantName,
|
||||
ResourceArn: `arn:aws:ses:${this.config.region}:${this.config.accountId}:identity/${domain}`,
|
||||
}),
|
||||
);
|
||||
this.logger.log(`Associated domain ${domain} with tenant ${tenantName}`);
|
||||
} catch (error) {
|
||||
if (error.name === 'AlreadyExistsException') {
|
||||
this.logger.log(
|
||||
`Domain ${domain} already associated with tenant ${tenantName}`,
|
||||
);
|
||||
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
private async enableDkimSigning(domain: string): Promise<void> {
|
||||
const sesClient = this.awsSesClientProvider.getSESClient();
|
||||
|
||||
const dkimCommand = new PutEmailIdentityDkimAttributesCommand({
|
||||
EmailIdentity: domain,
|
||||
SigningEnabled: true,
|
||||
});
|
||||
|
||||
await sesClient.send(dkimCommand);
|
||||
this.logger.log(`Enabled DKIM signing for domain: ${domain}`);
|
||||
}
|
||||
|
||||
private buildVerificationRecords(
|
||||
domain: string,
|
||||
dkimTokens: string[],
|
||||
): VerificationRecord[] {
|
||||
return dkimTokens.map((token) => ({
|
||||
type: 'CNAME' as const,
|
||||
key: `${token}._domainkey.${domain}`,
|
||||
value: `${token}.dkim.amazonses.com`,
|
||||
}));
|
||||
}
|
||||
|
||||
private determineVerificationStatus(identityResponse: {
|
||||
VerifiedForSendingStatus?: boolean;
|
||||
DkimAttributes?: {
|
||||
SigningEnabled?: boolean;
|
||||
Status?: string;
|
||||
};
|
||||
}): EmailingDomainStatus {
|
||||
const isVerified = identityResponse.VerifiedForSendingStatus === true;
|
||||
const isDkimEnabled =
|
||||
identityResponse.DkimAttributes?.SigningEnabled === true;
|
||||
const dkimStatus = identityResponse.DkimAttributes?.Status;
|
||||
|
||||
if (isVerified && isDkimEnabled && dkimStatus === 'SUCCESS') {
|
||||
return EmailingDomainStatus.VERIFIED;
|
||||
}
|
||||
|
||||
if (
|
||||
identityResponse.VerifiedForSendingStatus === false ||
|
||||
dkimStatus === 'FAILED'
|
||||
) {
|
||||
return EmailingDomainStatus.FAILED;
|
||||
}
|
||||
|
||||
return EmailingDomainStatus.PENDING;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,98 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type AwsSesError } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/types/aws-ses-error.type';
|
||||
import {
|
||||
EmailingDomainDriverException,
|
||||
EmailingDomainDriverExceptionCode,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/exceptions/emailing-domain-driver.exception';
|
||||
|
||||
@Injectable()
|
||||
export class AwsSesHandleErrorService {
|
||||
public handleAwsSesError(error: AwsSesError, context?: string): never {
|
||||
const name = error?.name ?? 'UnknownError';
|
||||
const message = error?.message ?? 'No message';
|
||||
const httpStatus = error?.$metadata?.httpStatusCode;
|
||||
const suffix = context ? ` (${context})` : '';
|
||||
|
||||
if (this.isTemporary(name, httpStatus)) {
|
||||
throw new EmailingDomainDriverException(
|
||||
`AWS SES temporary error${suffix}: ${message}`,
|
||||
EmailingDomainDriverExceptionCode.TEMPORARY_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isInsufficientPermissions(name, httpStatus)) {
|
||||
throw new EmailingDomainDriverException(
|
||||
`AWS SES insufficient permissions${suffix}: ${message}`,
|
||||
EmailingDomainDriverExceptionCode.INSUFFICIENT_PERMISSIONS,
|
||||
);
|
||||
}
|
||||
|
||||
if (this.isConfigurationError(name, httpStatus)) {
|
||||
throw new EmailingDomainDriverException(
|
||||
`AWS SES configuration error${suffix}: ${message}`,
|
||||
EmailingDomainDriverExceptionCode.CONFIGURATION_ERROR,
|
||||
);
|
||||
}
|
||||
|
||||
throw new EmailingDomainDriverException(
|
||||
`AWS SES error${suffix}: ${name} - ${message}`,
|
||||
EmailingDomainDriverExceptionCode.UNKNOWN,
|
||||
);
|
||||
}
|
||||
|
||||
private isTemporary(name: string, httpStatus?: number): boolean {
|
||||
if (httpStatus && httpStatus >= 500) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
name === 'ThrottlingException' ||
|
||||
name === 'ServiceUnavailable' ||
|
||||
name === 'InternalFailure' ||
|
||||
name === 'RequestTimeout' ||
|
||||
name === 'TooManyRequestsException'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isInsufficientPermissions(
|
||||
name: string,
|
||||
httpStatus?: number,
|
||||
): boolean {
|
||||
if (httpStatus === 403) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
name === 'AccessDeniedException' ||
|
||||
name === 'AccountSuspendedException'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private isConfigurationError(name: string, httpStatus?: number): boolean {
|
||||
if (httpStatus === 400) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
name === 'InvalidParameterValue' ||
|
||||
name === 'InvalidParameterCombination' ||
|
||||
name === 'MissingParameter' ||
|
||||
name === 'MessageRejected' ||
|
||||
name === 'MailFromDomainNotVerifiedException' ||
|
||||
name === 'FromEmailAddressNotVerifiedException'
|
||||
) {
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,8 @@
|
|||
export type AwsSesError = {
|
||||
name?: string;
|
||||
message?: string;
|
||||
$metadata?: {
|
||||
httpStatusCode?: number;
|
||||
requestId?: string;
|
||||
};
|
||||
};
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
|
||||
import { type AwsSesDriverConfig } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/driver-config.interface';
|
||||
import { type EmailingDomainDriverInterface } from 'src/engine/core-modules/emailing-domain/drivers/interfaces/emailing-domain-driver.interface';
|
||||
|
||||
import { AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider';
|
||||
import { AwsSesDriver } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-driver.service';
|
||||
import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { DriverFactoryBase } from 'src/engine/core-modules/twenty-config/dynamic-factory.base';
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
|
||||
@Injectable()
|
||||
export class EmailingDomainDriverFactory extends DriverFactoryBase<EmailingDomainDriverInterface> {
|
||||
constructor(
|
||||
twentyConfigService: TwentyConfigService,
|
||||
private readonly awsSesClientProvider: AwsSesClientProvider,
|
||||
private readonly awsSesHandleErrorService: AwsSesHandleErrorService,
|
||||
) {
|
||||
super(twentyConfigService);
|
||||
}
|
||||
|
||||
protected buildConfigKey(): string {
|
||||
const driver = EmailingDomainDriver.AWS_SES;
|
||||
|
||||
if (driver === EmailingDomainDriver.AWS_SES) {
|
||||
const awsConfigHash = this.getConfigGroupHash(
|
||||
ConfigVariablesGroup.AwsSesSettings,
|
||||
);
|
||||
|
||||
return `aws-ses|${awsConfigHash}`;
|
||||
}
|
||||
|
||||
throw new Error(`Unsupported emailing domain driver: ${driver}`);
|
||||
}
|
||||
|
||||
protected createDriver(): EmailingDomainDriverInterface {
|
||||
const driver = EmailingDomainDriver.AWS_SES;
|
||||
|
||||
switch (driver) {
|
||||
case EmailingDomainDriver.AWS_SES: {
|
||||
const region = this.twentyConfigService.get('AWS_SES_REGION');
|
||||
const accountId = this.twentyConfigService.get('AWS_SES_ACCOUNT_ID');
|
||||
const accessKeyId = this.twentyConfigService.get(
|
||||
'AWS_SES_ACCESS_KEY_ID',
|
||||
);
|
||||
const secretAccessKey = this.twentyConfigService.get(
|
||||
'AWS_SES_SECRET_ACCESS_KEY',
|
||||
);
|
||||
const sessionToken = this.twentyConfigService.get(
|
||||
'AWS_SES_SESSION_TOKEN',
|
||||
);
|
||||
|
||||
const awsConfig: AwsSesDriverConfig = {
|
||||
driver: EmailingDomainDriver.AWS_SES,
|
||||
region,
|
||||
accountId,
|
||||
accessKeyId,
|
||||
secretAccessKey,
|
||||
sessionToken,
|
||||
};
|
||||
|
||||
return new AwsSesDriver(
|
||||
awsConfig,
|
||||
this.awsSesClientProvider,
|
||||
this.awsSesHandleErrorService,
|
||||
);
|
||||
}
|
||||
|
||||
default:
|
||||
throw new Error(`Invalid emailing domain driver: ${driver}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
import { CustomException } from 'src/utils/custom-exception';
|
||||
|
||||
export class EmailingDomainDriverException extends CustomException<EmailingDomainDriverExceptionCode> {}
|
||||
|
||||
export enum EmailingDomainDriverExceptionCode {
|
||||
NOT_FOUND = 'NOT_FOUND',
|
||||
TEMPORARY_ERROR = 'TEMPORARY_ERROR',
|
||||
INSUFFICIENT_PERMISSIONS = 'INSUFFICIENT_PERMISSIONS',
|
||||
CONFIGURATION_ERROR = 'CONFIGURATION_ERROR',
|
||||
UNKNOWN = 'UNKNOWN',
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
import { type EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
|
||||
export interface BaseDriverConfig {
|
||||
driver: EmailingDomainDriver;
|
||||
}
|
||||
|
||||
export interface AwsSesDriverConfig extends BaseDriverConfig {
|
||||
driver: EmailingDomainDriver.AWS_SES;
|
||||
region: string;
|
||||
accountId: string;
|
||||
accessKeyId?: string;
|
||||
secretAccessKey?: string;
|
||||
sessionToken?: string;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { type EmailingDomainStatus } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { type VerificationRecord } from 'src/engine/core-modules/emailing-domain/drivers/types/verifications-record';
|
||||
|
||||
export type DomainVerificationInput = {
|
||||
domain: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type DomainStatusInput = {
|
||||
domain: string;
|
||||
workspaceId: string;
|
||||
};
|
||||
|
||||
export type EmailingDomainVerificationResult = {
|
||||
status: EmailingDomainStatus;
|
||||
verificationRecords: VerificationRecord[];
|
||||
verifiedAt: Date | null;
|
||||
};
|
||||
|
||||
export interface EmailingDomainDriverInterface {
|
||||
verifyDomain(
|
||||
input: DomainVerificationInput,
|
||||
): Promise<EmailingDomainVerificationResult>;
|
||||
getDomainStatus(
|
||||
input: DomainStatusInput,
|
||||
): Promise<EmailingDomainVerificationResult>;
|
||||
}
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
export enum EmailingDomainDriver {
|
||||
AWS_SES = 'AWS_SES',
|
||||
}
|
||||
|
||||
export enum EmailingDomainStatus {
|
||||
PENDING = 'PENDING',
|
||||
VERIFIED = 'VERIFIED',
|
||||
FAILED = 'FAILED',
|
||||
TEMPORARY_FAILURE = 'TEMPORARY_FAILURE',
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
export type VerificationRecord = {
|
||||
type: 'TXT' | 'CNAME' | 'MX';
|
||||
key: string;
|
||||
value: string;
|
||||
priority?: number;
|
||||
};
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
import { Field, ObjectType, registerEnumType } from '@nestjs/graphql';
|
||||
|
||||
import { IDField } from '@ptc-org/nestjs-query-graphql';
|
||||
|
||||
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
|
||||
import {
|
||||
EmailingDomainDriver,
|
||||
EmailingDomainStatus,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { VerificationRecord } from 'src/engine/core-modules/emailing-domain/dtos/verification-record.dto';
|
||||
|
||||
registerEnumType(EmailingDomainDriver, {
|
||||
name: 'EmailingDomainDriver',
|
||||
});
|
||||
|
||||
registerEnumType(EmailingDomainStatus, {
|
||||
name: 'EmailingDomainStatus',
|
||||
});
|
||||
|
||||
@ObjectType('EmailingDomain')
|
||||
export class EmailingDomainDto {
|
||||
@IDField(() => UUIDScalarType)
|
||||
id: string;
|
||||
|
||||
@Field(() => Date)
|
||||
createdAt: Date;
|
||||
|
||||
@Field(() => Date)
|
||||
updatedAt: Date;
|
||||
|
||||
@Field(() => String)
|
||||
domain: string;
|
||||
|
||||
@Field(() => EmailingDomainDriver)
|
||||
driver: EmailingDomainDriver;
|
||||
|
||||
@Field(() => EmailingDomainStatus)
|
||||
status: EmailingDomainStatus;
|
||||
|
||||
@Field(() => [VerificationRecord], { nullable: true })
|
||||
verificationRecords: VerificationRecord[] | null;
|
||||
|
||||
@Field(() => Date, { nullable: true })
|
||||
verifiedAt: Date | null;
|
||||
}
|
||||
|
|
@ -0,0 +1,16 @@
|
|||
import { Field, ObjectType } from '@nestjs/graphql';
|
||||
|
||||
@ObjectType()
|
||||
export class VerificationRecord {
|
||||
@Field(() => String)
|
||||
type: 'TXT' | 'CNAME' | 'MX';
|
||||
|
||||
@Field(() => String)
|
||||
key: string;
|
||||
|
||||
@Field(() => String)
|
||||
value: string;
|
||||
|
||||
@Field(() => Number, { nullable: true })
|
||||
priority?: number;
|
||||
}
|
||||
|
|
@ -0,0 +1,71 @@
|
|||
import { ObjectType } from '@nestjs/graphql';
|
||||
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
Entity,
|
||||
JoinColumn,
|
||||
ManyToOne,
|
||||
PrimaryGeneratedColumn,
|
||||
Unique,
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
|
||||
|
||||
import {
|
||||
EmailingDomainDriver,
|
||||
EmailingDomainStatus,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { VerificationRecord } from 'src/engine/core-modules/emailing-domain/drivers/types/verifications-record';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Entity({ name: 'emailingDomain', schema: 'core' })
|
||||
@ObjectType()
|
||||
@Unique('IDX_EMAILING_DOMAIN_DOMAIN_WORKSPACE_ID_UNIQUE', [
|
||||
'domain',
|
||||
'workspaceId',
|
||||
])
|
||||
export class EmailingDomain {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@CreateDateColumn({ type: 'timestamptz' })
|
||||
createdAt: Date;
|
||||
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt: Date;
|
||||
|
||||
@Column({ type: 'varchar', nullable: false })
|
||||
domain: string;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: Object.values(EmailingDomainDriver),
|
||||
nullable: false,
|
||||
})
|
||||
driver: EmailingDomainDriver;
|
||||
|
||||
@Column({
|
||||
type: 'enum',
|
||||
enum: Object.values(EmailingDomainStatus),
|
||||
default: EmailingDomainStatus.PENDING,
|
||||
nullable: false,
|
||||
})
|
||||
status: EmailingDomainStatus;
|
||||
|
||||
@Column({ type: 'jsonb', nullable: true })
|
||||
verificationRecords: VerificationRecord[];
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
verifiedAt: Date | null;
|
||||
|
||||
@Column({ nullable: false })
|
||||
workspaceId: string;
|
||||
|
||||
@ManyToOne(() => Workspace, (workspace) => workspace.emailingDomains, {
|
||||
onDelete: 'CASCADE',
|
||||
})
|
||||
@JoinColumn({ name: 'workspaceId' })
|
||||
workspace: Relation<Workspace>;
|
||||
}
|
||||
|
|
@ -0,0 +1,27 @@
|
|||
import { Module } from '@nestjs/common';
|
||||
|
||||
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
|
||||
|
||||
import { TypeORMModule } from 'src/database/typeorm/typeorm.module';
|
||||
import { AwsSesClientProvider } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/providers/aws-ses-client.provider';
|
||||
import { AwsSesHandleErrorService } from 'src/engine/core-modules/emailing-domain/drivers/aws-ses/services/aws-ses-handle-error.service';
|
||||
import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory';
|
||||
import { EmailingDomain } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { EmailingDomainResolver } from 'src/engine/core-modules/emailing-domain/emailing-domain.resolver';
|
||||
import { EmailingDomainService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
TypeORMModule,
|
||||
NestjsQueryTypeOrmModule.forFeature([EmailingDomain]),
|
||||
],
|
||||
exports: [EmailingDomainService],
|
||||
providers: [
|
||||
EmailingDomainService,
|
||||
EmailingDomainResolver,
|
||||
EmailingDomainDriverFactory,
|
||||
AwsSesClientProvider,
|
||||
AwsSesHandleErrorService,
|
||||
],
|
||||
})
|
||||
export class EmailingDomainModule {}
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
import { UseGuards, UsePipes } from '@nestjs/common';
|
||||
import { Args, Mutation, Query, Resolver } from '@nestjs/graphql';
|
||||
|
||||
import { EmailingDomainDriver } from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { EmailingDomainDto } from 'src/engine/core-modules/emailing-domain/dtos/emailing-domain.dto';
|
||||
import { EmailingDomainService } from 'src/engine/core-modules/emailing-domain/services/emailing-domain.service';
|
||||
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
|
||||
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
|
||||
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
|
||||
|
||||
@UseGuards(WorkspaceAuthGuard)
|
||||
@UsePipes(ResolverValidationPipe)
|
||||
@Resolver(() => EmailingDomainDto)
|
||||
export class EmailingDomainResolver {
|
||||
constructor(private readonly emailingDomainService: EmailingDomainService) {}
|
||||
|
||||
@Mutation(() => EmailingDomainDto)
|
||||
async createEmailingDomain(
|
||||
@Args('domain') domain: string,
|
||||
@Args('driver') driver: EmailingDomainDriver,
|
||||
@AuthWorkspace() currentWorkspace: Workspace,
|
||||
): Promise<EmailingDomainDto> {
|
||||
const emailingDomain =
|
||||
await this.emailingDomainService.createEmailingDomain(
|
||||
domain,
|
||||
driver,
|
||||
currentWorkspace,
|
||||
);
|
||||
|
||||
return emailingDomain;
|
||||
}
|
||||
|
||||
@Mutation(() => Boolean)
|
||||
async deleteEmailingDomain(
|
||||
@Args('id') id: string,
|
||||
@AuthWorkspace() currentWorkspace: Workspace,
|
||||
): Promise<boolean> {
|
||||
await this.emailingDomainService.deleteEmailingDomain(currentWorkspace, id);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@Mutation(() => EmailingDomainDto)
|
||||
async verifyEmailingDomain(
|
||||
@Args('id') id: string,
|
||||
@AuthWorkspace() currentWorkspace: Workspace,
|
||||
): Promise<EmailingDomainDto> {
|
||||
const emailingDomain =
|
||||
await this.emailingDomainService.verifyEmailingDomain(
|
||||
currentWorkspace,
|
||||
id,
|
||||
);
|
||||
|
||||
return emailingDomain;
|
||||
}
|
||||
|
||||
@Query(() => [EmailingDomainDto])
|
||||
async getEmailingDomains(
|
||||
@AuthWorkspace() currentWorkspace: Workspace,
|
||||
): Promise<EmailingDomainDto[]> {
|
||||
const emailingDomains =
|
||||
await this.emailingDomainService.getEmailingDomains(currentWorkspace);
|
||||
|
||||
return emailingDomains;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,173 @@
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
|
||||
import { Repository } from 'typeorm';
|
||||
|
||||
import { EmailingDomainDriverFactory } from 'src/engine/core-modules/emailing-domain/drivers/emailing-domain-driver.factory';
|
||||
import {
|
||||
EmailingDomainDriver,
|
||||
EmailingDomainStatus,
|
||||
} from 'src/engine/core-modules/emailing-domain/drivers/types/emailing-domain';
|
||||
import { EmailingDomain } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
|
||||
|
||||
@Injectable()
|
||||
export class EmailingDomainService {
|
||||
constructor(
|
||||
@InjectRepository(EmailingDomain)
|
||||
private readonly emailingDomainRepository: Repository<EmailingDomain>,
|
||||
private readonly emailingDomainDriverFactory: EmailingDomainDriverFactory,
|
||||
) {}
|
||||
|
||||
async createEmailingDomain(
|
||||
domain: string,
|
||||
driver: EmailingDomainDriver,
|
||||
workspace: Workspace,
|
||||
): Promise<EmailingDomain> {
|
||||
const existingDomain = await this.emailingDomainRepository.findOneBy({
|
||||
domain,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (existingDomain) {
|
||||
throw new Error('Emailing domain already exists for this workspace');
|
||||
}
|
||||
|
||||
const driverInstance = this.emailingDomainDriverFactory.getCurrentDriver();
|
||||
const verificationResult = await driverInstance.verifyDomain({
|
||||
domain,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
const domainToCreate = {
|
||||
domain,
|
||||
driver,
|
||||
workspaceId: workspace.id,
|
||||
...verificationResult,
|
||||
};
|
||||
|
||||
const savedDomain =
|
||||
await this.emailingDomainRepository.save(domainToCreate);
|
||||
|
||||
return savedDomain;
|
||||
}
|
||||
|
||||
async deleteEmailingDomain(
|
||||
workspace: Workspace,
|
||||
emailingDomainId: string,
|
||||
): Promise<void> {
|
||||
const emailingDomain = await this.emailingDomainRepository.findOneBy({
|
||||
id: emailingDomainId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
|
||||
if (!emailingDomain) {
|
||||
throw new Error('Emailing domain not found');
|
||||
}
|
||||
|
||||
await this.emailingDomainRepository.delete({
|
||||
id: emailingDomain.id,
|
||||
});
|
||||
}
|
||||
|
||||
async getEmailingDomains(workspace: Workspace): Promise<EmailingDomain[]> {
|
||||
return await this.emailingDomainRepository.find({
|
||||
where: {
|
||||
workspaceId: workspace.id,
|
||||
},
|
||||
order: {
|
||||
createdAt: 'DESC',
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async getEmailingDomain(
|
||||
workspace: Workspace,
|
||||
emailingDomainId: string,
|
||||
): Promise<EmailingDomain | null> {
|
||||
return await this.emailingDomainRepository.findOneBy({
|
||||
id: emailingDomainId,
|
||||
workspaceId: workspace.id,
|
||||
});
|
||||
}
|
||||
|
||||
async verifyEmailingDomain(
|
||||
workspace: Workspace,
|
||||
emailingDomainId: string,
|
||||
): Promise<EmailingDomain> {
|
||||
const emailingDomain = await this.getEmailingDomain(
|
||||
workspace,
|
||||
emailingDomainId,
|
||||
);
|
||||
|
||||
if (!emailingDomain) {
|
||||
throw new Error('Emailing domain not found');
|
||||
}
|
||||
|
||||
if (emailingDomain.status === EmailingDomainStatus.VERIFIED) {
|
||||
throw new Error('Emailing domain is already verified');
|
||||
}
|
||||
|
||||
const driver = this.emailingDomainDriverFactory.getCurrentDriver();
|
||||
const verificationResult = await driver.verifyDomain({
|
||||
domain: emailingDomain.domain,
|
||||
workspaceId: emailingDomain.workspaceId,
|
||||
});
|
||||
|
||||
const updatedDomain = await this.emailingDomainRepository.save({
|
||||
...emailingDomain,
|
||||
...verificationResult,
|
||||
});
|
||||
|
||||
return updatedDomain;
|
||||
}
|
||||
|
||||
async syncEmailingDomain(
|
||||
workspace: Workspace,
|
||||
emailingDomainId: string,
|
||||
): Promise<EmailingDomain> {
|
||||
const emailingDomain = await this.getEmailingDomain(
|
||||
workspace,
|
||||
emailingDomainId,
|
||||
);
|
||||
|
||||
if (!emailingDomain) {
|
||||
throw new Error('Emailing domain not found');
|
||||
}
|
||||
|
||||
await this.emailingDomainRepository.update(
|
||||
{
|
||||
id: emailingDomainId,
|
||||
},
|
||||
{
|
||||
verificationRecords: emailingDomain.verificationRecords,
|
||||
status: EmailingDomainStatus.PENDING,
|
||||
},
|
||||
);
|
||||
|
||||
try {
|
||||
const driver = this.emailingDomainDriverFactory.getCurrentDriver();
|
||||
const statusResult = await driver.getDomainStatus({
|
||||
domain: emailingDomain.domain,
|
||||
workspaceId: emailingDomain.workspaceId,
|
||||
});
|
||||
|
||||
const updatedDomain = await this.emailingDomainRepository.save({
|
||||
...emailingDomain,
|
||||
...statusResult,
|
||||
});
|
||||
|
||||
return updatedDomain;
|
||||
} catch (error) {
|
||||
await this.emailingDomainRepository.update(
|
||||
{ id: emailingDomainId },
|
||||
{
|
||||
verificationRecords: emailingDomain.verificationRecords,
|
||||
status: emailingDomain.status,
|
||||
},
|
||||
);
|
||||
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -18,5 +18,6 @@ export enum FeatureFlagKey {
|
|||
IS_CALENDAR_VIEW_ENABLED = 'IS_CALENDAR_VIEW_ENABLED',
|
||||
IS_GROUP_BY_ENABLED = 'IS_GROUP_BY_ENABLED',
|
||||
IS_PUBLIC_DOMAIN_ENABLED = 'IS_PUBLIC_DOMAIN_ENABLED',
|
||||
IS_EMAILING_DOMAIN_ENABLED = 'IS_EMAILING_DOMAIN_ENABLED',
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED = 'IS_DYNAMIC_SEARCH_FIELDS_ENABLED',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1211,6 +1211,50 @@ export class ConfigVariables {
|
|||
})
|
||||
@ValidateIf((env) => env.IS_MAPS_AND_ADDRESS_AUTOCOMPLETE_ENABLED)
|
||||
GOOGLE_MAP_API_KEY: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AwsSesSettings,
|
||||
description: 'AWS region',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsAWSRegion()
|
||||
@IsOptional()
|
||||
AWS_SES_REGION: AwsRegion;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AwsSesSettings,
|
||||
isSensitive: true,
|
||||
description: 'AWS access key ID',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsOptional()
|
||||
AWS_SES_ACCESS_KEY_ID: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AwsSesSettings,
|
||||
isSensitive: true,
|
||||
description: 'AWS session token',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsOptional()
|
||||
AWS_SES_SESSION_TOKEN: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AwsSesSettings,
|
||||
isSensitive: true,
|
||||
description: 'AWS secret access key',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsOptional()
|
||||
AWS_SES_SECRET_ACCESS_KEY: string;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.AwsSesSettings,
|
||||
description: 'AWS Account ID for SES ARN construction',
|
||||
type: ConfigVariableType.STRING,
|
||||
})
|
||||
@IsOptional()
|
||||
AWS_SES_ACCOUNT_ID: string;
|
||||
}
|
||||
|
||||
export const validate = (config: Record<string, unknown>): ConfigVariables => {
|
||||
|
|
|
|||
|
|
@ -125,4 +125,9 @@ export const CONFIG_VARIABLES_GROUP_METADATA: Record<
|
|||
'These have been set to sensible default so you probably don’t need to change them unless you have a specific use-case.',
|
||||
isHiddenOnLoad: true,
|
||||
},
|
||||
[ConfigVariablesGroup.AwsSesSettings]: {
|
||||
position: 2100,
|
||||
description: 'Configure AWS SES settings for emailing domains',
|
||||
isHiddenOnLoad: true,
|
||||
},
|
||||
};
|
||||
|
|
|
|||
|
|
@ -19,4 +19,5 @@ export enum ConfigVariablesGroup {
|
|||
AnalyticsConfig = 'audit-config',
|
||||
TokensDuration = 'tokens-duration',
|
||||
TwoFactorAuthentication = 'two-factor-authentication',
|
||||
AwsSesSettings = 'aws-ses-settings',
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,9 +19,11 @@ import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/
|
|||
import { ApiKey } from 'src/engine/core-modules/api-key/api-key.entity';
|
||||
import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity';
|
||||
import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity';
|
||||
import { EmailingDomain } from 'src/engine/core-modules/emailing-domain/emailing-domain.entity';
|
||||
import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity';
|
||||
import { KeyValuePair } from 'src/engine/core-modules/key-value-pair/key-value-pair.entity';
|
||||
import { PostgresCredentials } from 'src/engine/core-modules/postgres-credentials/postgres-credentials.entity';
|
||||
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
|
||||
import { WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity';
|
||||
import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity';
|
||||
import { ViewFieldDTO } from 'src/engine/core-modules/view/dtos/view-field.dto';
|
||||
|
|
@ -41,7 +43,6 @@ import { AgentHandoffEntity } from 'src/engine/metadata-modules/agent/agent-hand
|
|||
import { AgentEntity } from 'src/engine/metadata-modules/agent/agent.entity';
|
||||
import { AgentDTO } from 'src/engine/metadata-modules/agent/dtos/agent.dto';
|
||||
import { RoleDTO } from 'src/engine/metadata-modules/role/dtos/role.dto';
|
||||
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
|
||||
|
||||
registerEnumType(WorkspaceActivationStatus, {
|
||||
name: 'WorkspaceActivationStatus',
|
||||
|
|
@ -116,6 +117,9 @@ export class Workspace {
|
|||
)
|
||||
approvedAccessDomains: Relation<ApprovedAccessDomain[]>;
|
||||
|
||||
@OneToMany(() => EmailingDomain, (emailingDomain) => emailingDomain.workspace)
|
||||
emailingDomains: Relation<EmailingDomain[]>;
|
||||
|
||||
@OneToMany(() => PublicDomain, (publicDomain) => publicDomain.workspace)
|
||||
publicDomains: Relation<PublicDomain[]>;
|
||||
|
||||
|
|
|
|||
|
|
@ -141,6 +141,7 @@ describe('WorkspaceEntityManager', () => {
|
|||
IS_CALENDAR_VIEW_ENABLED: false,
|
||||
IS_GROUP_BY_ENABLED: false,
|
||||
IS_PUBLIC_DOMAIN_ENABLED: false,
|
||||
IS_EMAILING_DOMAIN_ENABLED: false,
|
||||
IS_DYNAMIC_SEARCH_FIELDS_ENABLED: false,
|
||||
},
|
||||
eventEmitterService: {
|
||||
|
|
|
|||
|
|
@ -83,7 +83,12 @@ export const seedFeatureFlags = async (
|
|||
{
|
||||
key: FeatureFlagKey.IS_PUBLIC_DOMAIN_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: false,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_EMAILING_DOMAIN_ENABLED,
|
||||
workspaceId: workspaceId,
|
||||
value: true,
|
||||
},
|
||||
{
|
||||
key: FeatureFlagKey.IS_DYNAMIC_SEARCH_FIELDS_ENABLED,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@ export enum SettingsPath {
|
|||
Domain = 'domains/domain',
|
||||
PublicDomain = 'domains/public-domain',
|
||||
NewApprovedAccessDomain = 'domains/approved-access-domain/new',
|
||||
NewEmailingDomain = 'domains/emailing-domain/new',
|
||||
EmailingDomainDetail = 'domains/emailing-domain/:domainId',
|
||||
Releases = 'releases',
|
||||
AI = 'ai',
|
||||
AINewAgent = 'ai/new-agent',
|
||||
|
|
|
|||
Loading…
Reference in a new issue