[Breaking: DEPLOY SERVER BEFORE FRONT] fix: allow custom domain without Cloudflare API key (#17160)
## Summary Fixes #17101 When self-hosting TwentyCRM, users can now set workspace custom domains without requiring CLOUDFLARE_API_KEY to be configured. This enables manual DNS configuration for those not using Cloudflare. ## Changes - Added isCloudflareConfigured method to DnsManagerService - Modified all Cloudflare-dependent methods to gracefully handle missing configuration - Added getManualDnsRecords helper that provides DNS configuration instructions when Cloudflare is not available - isHostnameWorking returns true in manual mode, allowing the domain to be saved and enabled - Added comprehensive tests for non-Cloudflare scenarios ## Behavior When CLOUDFLARE_API_KEY is set: Works exactly as before with automatic Cloudflare provisioning When CLOUDFLARE_API_KEY is NOT set: - Custom domain can be saved to the database - User receives manual DNS configuration instructions - Domain is marked as working, user is responsible for external DNS and TLS configuration ## Testing Added tests covering non-Cloudflare scenarios. Full test suite requires Docker which was not run locally. <!-- CURSOR_SUMMARY --> --- > [!NOTE] > Introduces a Cloudflare integration feature flag and wires it through server and front to gate the custom domain UI. > > - Server: adds `isCloudflareIntegrationEnabled` to `ClientConfig`, computed from `CLOUDFLARE_API_KEY` and `CLOUDFLARE_ZONE_ID` in `client-config.service`; updates GraphQL schema and unit tests. > - Frontend: adds `isCloudflareIntegrationEnabledState`, extends `ClientConfig` type, sets the flag in `useClientConfig`, and conditionally renders `SettingsCustomDomain` in `SettingsDomain` when enabled. > - Updates mocked client config to include the new flag. > > <sup>Written by [Cursor Bugbot](https://cursor.com/dashboard?tab=bugbot) for commit 0c76dde03b1ae940a9dae65e8673c3ba4cfec9dc. This will update automatically on new commits. Configure [here](https://cursor.com/dashboard?tab=bugbot).</sup> <!-- /CURSOR_SUMMARY --> --------- Signed-off-by: Vedant Madane <6527493+VedantMadane@users.noreply.github.com> Co-authored-by: Félix Malfait <felix@twenty.com> Co-authored-by: Félix Malfait <felix.malfait@gmail.com>
This commit is contained in:
@@ -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 { isCloudflareIntegrationEnabledState } from '@/client-config/states/isCloudflareIntegrationEnabledState';
|
||||
import { isEmailingDomainsEnabledState } from '@/client-config/states/isEmailingDomainsEnabledState';
|
||||
import { isEmailVerificationRequiredState } from '@/client-config/states/isEmailVerificationRequiredState';
|
||||
import { isGoogleCalendarEnabledState } from '@/client-config/states/isGoogleCalendarEnabledState';
|
||||
@@ -109,6 +110,9 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
const setIsEmailingDomainsEnabled = useSetRecoilState(
|
||||
isEmailingDomainsEnabledState,
|
||||
);
|
||||
const setIsCloudflareIntegrationEnabled = useSetRecoilState(
|
||||
isCloudflareIntegrationEnabledState,
|
||||
);
|
||||
|
||||
const setAppVersion = useSetRecoilState(appVersionState);
|
||||
|
||||
@@ -184,6 +188,9 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setCalendarBookingPageId(clientConfig?.calendarBookingPageId ?? null);
|
||||
setIsImapSmtpCaldavEnabled(clientConfig?.isImapSmtpCaldavEnabled);
|
||||
setIsEmailingDomainsEnabled(clientConfig?.isEmailingDomainsEnabled);
|
||||
setIsCloudflareIntegrationEnabled(
|
||||
clientConfig?.isCloudflareIntegrationEnabled,
|
||||
);
|
||||
} catch (err) {
|
||||
const error =
|
||||
err instanceof Error ? err : new Error('Failed to fetch client config');
|
||||
@@ -217,6 +224,7 @@ export const useClientConfig = (): UseClientConfigResult => {
|
||||
setIsImapSmtpCaldavEnabled,
|
||||
setIsMultiWorkspaceEnabled,
|
||||
setIsEmailingDomainsEnabled,
|
||||
setIsCloudflareIntegrationEnabled,
|
||||
setLabPublicFeatureFlags,
|
||||
setMicrosoftCalendarEnabled,
|
||||
setMicrosoftMessagingEnabled,
|
||||
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
import { createState } from 'twenty-ui/utilities';
|
||||
|
||||
export const isCloudflareIntegrationEnabledState = createState<boolean>({
|
||||
key: 'isCloudflareIntegrationEnabled',
|
||||
defaultValue: false,
|
||||
});
|
||||
@@ -32,6 +32,7 @@ export type ClientConfig = {
|
||||
isMultiWorkspaceEnabled: boolean;
|
||||
isImapSmtpCaldavEnabled: boolean;
|
||||
isEmailingDomainsEnabled: boolean;
|
||||
isCloudflareIntegrationEnabled: boolean;
|
||||
publicFeatureFlags: Array<PublicFeatureFlag>;
|
||||
sentry: Sentry;
|
||||
signInPrefilled: boolean;
|
||||
|
||||
@@ -13,7 +13,7 @@ import { ApolloError } from '@apollo/client';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { Trans, useLingui } from '@lingui/react/macro';
|
||||
import { FormProvider, useForm } from 'react-hook-form';
|
||||
import { useRecoilState } from 'recoil';
|
||||
import { useRecoilState, useRecoilValue } from 'recoil';
|
||||
import { SettingsPath } from 'twenty-shared/types';
|
||||
import { getSettingsPath, isDefined } from 'twenty-shared/utils';
|
||||
import { z } from 'zod';
|
||||
@@ -25,6 +25,7 @@ import { useState } from 'react';
|
||||
import { getSubdomainValidationSchema } from '@/settings/domains/utils/get-subdomain-validation-schema';
|
||||
import { getDomainValidationSchema } from '@/settings/domains/utils/get-domain-validation-schema';
|
||||
import { useCheckCustomDomainValidRecords } from '@/settings/domains/hooks/useCheckCustomDomainValidRecords';
|
||||
import { isCloudflareIntegrationEnabledState } from '@/client-config/states/isCloudflareIntegrationEnabledState';
|
||||
|
||||
export const SUBDOMAIN_CHANGE_CONFIRMATION_MODAL_ID =
|
||||
'subdomain-change-confirmation-modal';
|
||||
@@ -33,6 +34,9 @@ export const SettingsDomain = () => {
|
||||
const navigate = useNavigateSettings();
|
||||
const { checkCustomDomainRecords } = useCheckCustomDomainValidRecords();
|
||||
const { t } = useLingui();
|
||||
const isCloudflareIntegrationEnabled = useRecoilValue(
|
||||
isCloudflareIntegrationEnabledState,
|
||||
);
|
||||
|
||||
const validationSchema = z
|
||||
.object({
|
||||
@@ -226,7 +230,7 @@ export const SettingsDomain = () => {
|
||||
>
|
||||
<SettingsPageContainer>
|
||||
<SettingsSubdomain />
|
||||
<SettingsCustomDomain />
|
||||
{isCloudflareIntegrationEnabled && <SettingsCustomDomain />}
|
||||
</SettingsPageContainer>
|
||||
</SubMenuTopBarContainer>
|
||||
</FormProvider>
|
||||
|
||||
@@ -56,4 +56,5 @@ export const mockedClientConfig: ClientConfig = {
|
||||
isImapSmtpCaldavEnabled: false,
|
||||
isTwoFactorAuthenticationEnabled: false,
|
||||
isEmailingDomainsEnabled: false,
|
||||
isCloudflareIntegrationEnabled: false,
|
||||
};
|
||||
|
||||
+1
@@ -98,6 +98,7 @@ describe('ClientConfigController', () => {
|
||||
isImapSmtpCaldavEnabled: false,
|
||||
calendarBookingPageId: undefined,
|
||||
isTwoFactorAuthenticationEnabled: false,
|
||||
isCloudflareIntegrationEnabled: false,
|
||||
};
|
||||
|
||||
jest
|
||||
|
||||
@@ -197,4 +197,7 @@ export class ClientConfig {
|
||||
|
||||
@Field(() => String, { nullable: true })
|
||||
calendarBookingPageId?: string;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isCloudflareIntegrationEnabled: boolean;
|
||||
}
|
||||
|
||||
+5
@@ -86,7 +86,10 @@ describe('ClientConfigService', () => {
|
||||
MESSAGING_PROVIDER_GMAIL_ENABLED: true,
|
||||
CALENDAR_PROVIDER_GOOGLE_ENABLED: true,
|
||||
IS_CONFIG_VARIABLES_IN_DB_ENABLED: false,
|
||||
IS_IMAP_SMTP_CALDAV_ENABLED: false,
|
||||
CALENDAR_BOOKING_PAGE_ID: 'team/twenty/talk-to-us',
|
||||
CLOUDFLARE_API_KEY: undefined,
|
||||
CLOUDFLARE_ZONE_ID: undefined,
|
||||
};
|
||||
|
||||
return mockValues[key];
|
||||
@@ -155,7 +158,9 @@ describe('ClientConfigService', () => {
|
||||
isGoogleMessagingEnabled: true,
|
||||
isGoogleCalendarEnabled: true,
|
||||
isConfigVariablesInDbEnabled: false,
|
||||
isImapSmtpCaldavEnabled: false,
|
||||
calendarBookingPageId: 'team/twenty/talk-to-us',
|
||||
isCloudflareIntegrationEnabled: false,
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
+8
@@ -29,6 +29,13 @@ export class ClientConfigService {
|
||||
private aiModelRegistryService: AiModelRegistryService,
|
||||
) {}
|
||||
|
||||
private isCloudflareIntegrationEnabled(): boolean {
|
||||
return (
|
||||
!!this.twentyConfigService.get('CLOUDFLARE_API_KEY') &&
|
||||
!!this.twentyConfigService.get('CLOUDFLARE_ZONE_ID')
|
||||
);
|
||||
}
|
||||
|
||||
async getClientConfig(): Promise<ClientConfig> {
|
||||
const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER');
|
||||
const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER');
|
||||
@@ -191,6 +198,7 @@ export class ClientConfigService {
|
||||
calendarBookingPageId: isNonEmptyString(calendarBookingPageId)
|
||||
? calendarBookingPageId
|
||||
: undefined,
|
||||
isCloudflareIntegrationEnabled: this.isCloudflareIntegrationEnabled(),
|
||||
};
|
||||
|
||||
return clientConfig;
|
||||
|
||||
Reference in New Issue
Block a user