diff --git a/packages/twenty-server/src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter.ts b/packages/twenty-server/src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter.ts index 5234f2dbd25..953aece612b 100644 --- a/packages/twenty-server/src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter.ts +++ b/packages/twenty-server/src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter.ts @@ -6,19 +6,26 @@ import { DnsManagerException, DnsManagerExceptionCode, } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception'; +import { + BaseGraphQLError, + ConflictError, + NotFoundError, +} from 'src/engine/core-modules/graphql/utils/graphql-errors.util'; @Catch(DnsManagerException) export class DnsManagerExceptionFilter implements ExceptionFilter { catch(exception: DnsManagerException) { switch (exception.code) { - case DnsManagerExceptionCode.INTERNAL_SERVER_ERROR: - case DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED: case DnsManagerExceptionCode.HOSTNAME_NOT_REGISTERED: + case DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL: + throw new NotFoundError(exception); + case DnsManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED: + throw new ConflictError(exception); case DnsManagerExceptionCode.INVALID_INPUT_DATA: case DnsManagerExceptionCode.CLOUDFLARE_CLIENT_NOT_INITIALIZED: case DnsManagerExceptionCode.MULTIPLE_HOSTNAMES_FOUND: - case DnsManagerExceptionCode.MISSING_PUBLIC_DOMAIN_URL: - throw exception; + case DnsManagerExceptionCode.INTERNAL_SERVER_ERROR: + throw new BaseGraphQLError(exception); default: { assertUnreachable(exception.code); } diff --git a/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.spec.ts b/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.spec.ts index 10fdd60e8c4..4613742e01b 100644 --- a/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.spec.ts @@ -288,6 +288,63 @@ describe('DnsManagerService', () => { }); }); + describe('refreshHostname', () => { + it('should throw DnsManagerException when hostname is not found in Cloudflare', async () => { + const hostname = 'example.com'; + const cloudflareMock = { + customHostnames: { + list: jest.fn().mockResolvedValueOnce({ result: [] }), + }, + }; + + jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id'); + (dnsManagerService as any).cloudflareClient = cloudflareMock; + + await expect( + dnsManagerService.refreshHostname(hostname), + ).rejects.toThrow(DnsManagerException); + }); + + it('should refresh and return hostname records when hostname exists', async () => { + const hostname = 'example.com'; + const mockResult = { + id: 'custom-id', + hostname, + verification_errors: [], + ssl: { + dcv_delegation_records: [], + }, + }; + const cloudflareMock = { + customHostnames: { + list: jest.fn().mockResolvedValueOnce({ result: [mockResult] }), + edit: jest.fn().mockResolvedValueOnce({}), + }, + }; + + jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id'); + jest + .spyOn(domainServerConfigService, 'getBaseUrl') + .mockReturnValue(new URL('https://front.domain')); + (dnsManagerService as any).cloudflareClient = cloudflareMock; + + const result = await dnsManagerService.refreshHostname(hostname); + + expect(result).toEqual({ + id: 'custom-id', + domain: hostname, + records: expect.any(Array), + }); + expect(cloudflareMock.customHostnames.edit).toHaveBeenCalledWith( + 'custom-id', + { + zone_id: 'test-zone-id', + ssl: expect.any(Object), + }, + ); + }); + }); + describe('updateHostname', () => { it('should update a custom domain and register a new one', async () => { const fromHostname = 'old.com'; diff --git a/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.ts b/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.ts index d02693873bd..9b65ec87623 100644 --- a/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/dns-manager/services/dns-manager.service.ts @@ -138,7 +138,16 @@ export class DnsManagerService { options, ); - assertIsDefinedOrThrow(publicDomainWithRecords); + assertIsDefinedOrThrow( + publicDomainWithRecords, + new DnsManagerException( + 'Hostname not found in Cloudflare', + DnsManagerExceptionCode.HOSTNAME_NOT_REGISTERED, + { + userFriendlyMessage: msg`Domain is not registered in Cloudflare`, + }, + ), + ); await this.cloudflareClient.customHostnames.edit( publicDomainWithRecords.id, diff --git a/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.resolver.ts b/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.resolver.ts index 59c74b558be..c7425f29c82 100644 --- a/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/public-domain/public-domain.resolver.ts @@ -8,6 +8,7 @@ import { PermissionFlagType } from 'twenty-shared/constants'; import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator'; import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records'; +import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter'; import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe'; @@ -31,6 +32,7 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; ) @UsePipes(ResolverValidationPipe) @UseFilters( + DnsManagerExceptionFilter, PublicDomainExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter, ) diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts index 6e4e0a4e0e3..01a904dc9b9 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.resolver.ts @@ -23,6 +23,7 @@ import { BillingEntitlementDTO } from 'src/engine/core-modules/billing/dtos/bill import { BillingSubscriptionEntity } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records'; +import { DnsManagerExceptionFilter } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager-exception-filter'; import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service'; import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; @@ -82,6 +83,7 @@ const OriginHeader = createParamDecorator( @MetadataResolver(() => WorkspaceEntity) @UsePipes(ResolverValidationPipe) @UseFilters( + DnsManagerExceptionFilter, PreventNestToAutoLogGraphqlErrorsFilter, PermissionsGraphqlApiExceptionFilter, )