From 26e432d8e249c198fc796c42cb9a25b2986305d4 Mon Sep 17 00:00:00 2001 From: Naineel Soyantar <112230479+naineel1209@users.noreply.github.com> Date: Tue, 21 Oct 2025 20:23:48 +0530 Subject: [PATCH] fix: Add reserved subdomains constant and update validation on `generateSubdomain` (#15217) ## Description Fixes #15160 - Moved the reserved subdomains to a separate shared constant file: `packages/twenty-server/src/engine/core-modules/workspace/constants/reserved-subdomains.constant.ts` - Updated the validation while generating subdomain to check if the extracted subdomain (from email or display name) is reserved - When a reserved subdomain is detected, the server will automatically fall back to a random subdomain --------- Co-authored-by: Naineel Soyantar Co-authored-by: Charles Bochet --- .../core-query-builder.factory.ts | 8 +- .../core-query-builder.module.ts | 4 +- .../api/rest/core/rest-api-core.module.ts | 2 - .../__tests__/admin-panel.service.spec.ts | 4 +- .../admin-panel/admin-panel.module.ts | 4 +- .../admin-panel/admin-panel.service.ts | 10 +- .../approved-access-domain.module.ts | 4 +- .../approved-access-domain.service.ts | 6 +- .../services/approved-access-domain.spec.ts | 15 +- .../engine/core-modules/auth/auth.module.ts | 8 +- .../core-modules/auth/auth.resolver.spec.ts | 4 +- .../engine/core-modules/auth/auth.resolver.ts | 31 +- .../google-apis-auth.controller.ts | 6 +- .../microsoft-apis-auth.controller.ts | 6 +- .../auth/controllers/sso-auth.controller.ts | 10 +- .../filters/auth-oauth-exception.filter.ts | 6 +- ...pis-oauth-exchange-code-for-token.guard.ts | 6 +- .../google-apis-oauth-request-code.guard.ts | 6 +- .../auth/guards/google-oauth.guard.ts | 8 +- ...microsoft-apis-oauth-request-code.guard.ts | 6 +- .../auth/guards/microsoft-oauth.guard.ts | 6 +- .../auth/guards/oidc-auth.guard.ts | 8 +- .../auth/guards/oidc-auth.spec.ts | 10 +- .../auth/guards/saml-auth.guard.ts | 8 +- .../auth/services/auth.service.spec.ts | 9 +- .../auth/services/auth.service.ts | 20 +- .../services/reset-password.service.spec.ts | 24 +- .../auth/services/reset-password.service.ts | 8 +- .../auth/services/sign-in-up.service.spec.ts | 13 +- .../auth/services/sign-in-up.service.ts | 8 +- .../billing-webhook/billing-webhook.module.ts | 4 +- .../core-modules/billing/billing.module.ts | 8 +- .../billing-portal.workspace-service.ts | 16 +- .../services/stripe-billing-portal.service.ts | 6 +- .../billing/stripe/stripe.module.ts | 8 +- .../client-config/client-config.module.ts | 4 +- .../services/client-config.service.spec.ts | 13 +- .../services/client-config.service.ts | 6 +- .../cloudflare/cloudflare.module.ts | 12 +- .../services/dns-cloudflare.service.ts | 14 +- .../dns-manager/dns-manager.module.ts | 5 +- .../services/dns-manager.service.spec.ts | 23 +- .../services/dns-manager.service.ts | 10 +- .../domain-manager/domain-manager.module.ts | 14 - .../custom-domain-manager.module.ts | 21 + .../services/custom-domain-manager.service.ts | 127 ++++++ .../domain-server-config.module.ts | 10 + .../domain-server-config.service.spec.ts | 82 ++++ .../services/domain-server-config.service.ts | 69 ++++ .../utils/append-search-params-to-url.util.ts | 8 + ...rl-with-pathname-and-search-params.util.ts | 25 ++ .../services/subdomain-manager.service.ts | 85 ++++ .../subdomain-manager.module.ts | 12 + .../generate-random-subdomain.util.spec.ts} | 2 +- .../get-subdomain-from-email.util.spec.ts} | 2 +- ...omain-name-from-display-name.util.spec.ts} | 2 +- .../__test__/is-subdomain-valid.util.spec.ts | 384 ++++++++++++++++++ .../utils/generate-random-subdomain.util.ts} | 0 .../utils/get-subdomain-from-email.util.ts} | 2 +- ...-subdomain-name-from-display-name.util.ts} | 0 .../utils/is-subdomain-valid.util.ts | 9 + .../workspace-domains.service.spec.ts} | 116 ++---- .../services/workspace-domains.service.ts} | 145 ++----- .../types/workspace-domain-config.type.ts} | 2 +- .../workspace-domains.module.ts | 17 + .../email-verification.module.ts | 8 +- .../email-verification.resolver.ts | 6 +- .../services/email-verification.service.ts | 22 +- .../guard-redirect/guard-redirect.module.ts | 5 +- .../services/guard-redirect.service.ts | 12 +- .../__tests__/impersonation.service.spec.ts | 4 +- .../impersonation/impersonation.module.ts | 4 +- .../services/impersonation.service.ts | 6 +- .../src/engine/core-modules/sso/sso.module.ts | 2 - .../two-factor-authentication.module.ts | 6 +- ...two-factor-authentication.resolver.spec.ts | 20 +- .../two-factor-authentication.resolver.ts | 6 +- .../user-workspace/user-workspace.module.ts | 4 +- .../user-workspace.service.spec.ts | 4 +- .../user-workspace/user-workspace.service.ts | 6 +- .../workspace-invitation.service.spec.ts | 4 +- .../services/workspace-invitation.service.ts | 6 +- .../workspace-invitation.module.ts | 4 +- .../constants/reserved-subdomains.constant.ts | 122 ++++++ .../valid-subdomain-pattern.constant.ts | 2 + ...ck-custom-domain-valid-records.cron.job.ts | 12 +- .../workspace/dtos/update-workspace-input.ts | 124 ------ .../__tests__/workspace.service.spec.ts | 6 +- .../workspace/services/workspace.service.ts | 141 +------ ...pace-graphql-api-exception-handler.util.ts | 1 + .../workspace/workspace.exception.ts | 1 + .../workspace/workspace.module.ts | 8 +- .../workspace/workspace.resolver.ts | 14 +- .../agent/agent-execution.service.ts | 6 +- .../metadata-modules/agent/agent.module.ts | 4 +- .../route-trigger/route-trigger.module.ts | 4 +- .../route-trigger/route-trigger.service.ts | 18 +- 97 files changed, 1369 insertions(+), 754 deletions(-) delete mode 100644 packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/domain-server-config.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/__test__/domain-server-config.service.spec.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/append-search-params-to-url.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service.ts create mode 100644 packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module.ts rename packages/twenty-server/src/engine/core-modules/{domain-manager/utils/__test__/generate-random-subdomain.spec.ts => domain/subdomain-manager/utils/__test__/generate-random-subdomain.util.spec.ts} (88%) rename packages/twenty-server/src/engine/core-modules/{domain-manager/utils/__test__/get-subdomain-from-email.spec.ts => domain/subdomain-manager/utils/__test__/get-subdomain-from-email.util.spec.ts} (91%) rename packages/twenty-server/src/engine/core-modules/{domain-manager/utils/__test__/get-subdomain-name-from-display-name.spec.ts => domain/subdomain-manager/utils/__test__/get-subdomain-name-from-display-name.util.spec.ts} (94%) create mode 100644 packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/is-subdomain-valid.util.spec.ts rename packages/twenty-server/src/engine/core-modules/{domain-manager/utils/generate-random-subdomain.ts => domain/subdomain-manager/utils/generate-random-subdomain.util.ts} (100%) rename packages/twenty-server/src/engine/core-modules/{domain-manager/utils/get-subdomain-from-email.ts => domain/subdomain-manager/utils/get-subdomain-from-email.util.ts} (88%) rename packages/twenty-server/src/engine/core-modules/{domain-manager/utils/get-subdomain-name-from-display-name.ts => domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util.ts} (100%) create mode 100644 packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util.ts rename packages/twenty-server/src/engine/core-modules/{domain-manager/services/domain-manager.service.spec.ts => domain/workspace-domains/services/__test__/workspace-domains.service.spec.ts} (81%) rename packages/twenty-server/src/engine/core-modules/{domain-manager/services/domain-manager.service.ts => domain/workspace-domains/services/workspace-domains.service.ts} (52%) rename packages/twenty-server/src/engine/core-modules/{domain-manager/domain-manager.type.ts => domain/workspace-domains/types/workspace-domain-config.type.ts} (66%) create mode 100644 packages/twenty-server/src/engine/core-modules/domain/workspace-domains/workspace-domains.module.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workspace/constants/reserved-subdomains.constant.ts create mode 100644 packages/twenty-server/src/engine/core-modules/workspace/constants/valid-subdomain-pattern.constant.ts rename packages/twenty-server/src/engine/core-modules/workspace/{ => services}/__tests__/workspace.service.spec.ts (97%) diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts index 6ff3b88649..071b25f393 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.factory.ts @@ -13,8 +13,9 @@ import { parseCoreBatchPath } from 'src/engine/api/rest/core/query-builder/utils import { parseCorePath } from 'src/engine/api/rest/core/query-builder/utils/path-parsers/parse-core-path.utils'; import { type Query } from 'src/engine/api/rest/core/types/query.type'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; +import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; import { type ObjectMetadataItemWithFieldMaps } from 'src/engine/metadata-modules/types/object-metadata-item-with-field-maps'; import { type ObjectMetadataMaps } from 'src/engine/metadata-modules/types/object-metadata-maps'; import { getObjectMetadataMapItemByNamePlural } from 'src/engine/metadata-modules/utils/get-object-metadata-map-item-by-name-plural.util'; @@ -23,7 +24,6 @@ import { WorkspaceMetadataCacheService } from 'src/engine/metadata-modules/works import { WorkspaceCacheStorageService } from 'src/engine/workspace-cache-storage/workspace-cache-storage.service'; import { standardObjectMetadataDefinitions } from 'src/engine/workspace-manager/workspace-sync-metadata/standard-objects'; import { shouldExcludeFromWorkspaceApi } from 'src/engine/workspace-manager/workspace-sync-metadata/utils/should-exclude-from-workspace-api.util'; -import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @Injectable() export class CoreQueryBuilderFactory { @@ -33,7 +33,7 @@ export class CoreQueryBuilderFactory { private readonly findDuplicatesQueryFactory: FindDuplicatesQueryFactory, private readonly findDuplicatesVariablesFactory: FindDuplicatesVariablesFactory, private readonly accessTokenService: AccessTokenService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, private readonly workspaceMetadataCacheService: WorkspaceMetadataCacheService, private readonly featureFlagService: FeatureFlagService, @@ -69,7 +69,7 @@ export class CoreQueryBuilderFactory { if (!objectMetadataMaps) { throw new BadRequestException( - `No object was found for the workspace associated with this API key. You may generate a new one here ${this.domainManagerService + `No object was found for the workspace associated with this API key. You may generate a new one here ${this.workspaceDomainsService .buildWorkspaceURL({ workspace, pathname: getSettingsPath(SettingsPath.ApiWebhooks), diff --git a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts index 72d7847bb2..0af8f2b3b6 100644 --- a/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/query-builder/core-query-builder.module.ts @@ -4,7 +4,7 @@ import { CoreQueryBuilderFactory } from 'src/engine/api/rest/core/query-builder/ import { coreQueryBuilderFactories } from 'src/engine/api/rest/core/query-builder/factories/factories'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { WorkspaceMetadataCacheModule } from 'src/engine/metadata-modules/workspace-metadata-cache/workspace-metadata-cache.module'; import { WorkspacePermissionsCacheModule } from 'src/engine/metadata-modules/workspace-permissions-cache/workspace-permissions-cache.module'; @@ -14,7 +14,7 @@ import { WorkspaceCacheStorageModule } from 'src/engine/workspace-cache-storage/ imports: [ AuthModule, ApiKeyModule, - DomainManagerModule, + WorkspaceDomainsModule, FeatureFlagModule, WorkspaceCacheStorageModule, WorkspaceMetadataCacheModule, diff --git a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts index 7ee46bcf01..f5a6791b5d 100644 --- a/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts +++ b/packages/twenty-server/src/engine/api/rest/core/rest-api-core.module.ts @@ -19,7 +19,6 @@ import { RestApiService } from 'src/engine/api/rest/rest-api.service'; import { ActorModule } from 'src/engine/core-modules/actor/actor.module'; import { ApiKeyModule } from 'src/engine/core-modules/api-key/api-key.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { RecordTransformerModule } from 'src/engine/core-modules/record-transformer/record-transformer.module'; import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role.module'; @@ -54,7 +53,6 @@ const restApiCoreResolvers = [ ActorModule, FeatureFlagModule, CoreCommonApiModule, - DomainManagerModule, ], controllers: [RestApiCoreController], providers: [ diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts index ee5bb9cf00..bb550cc01c 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/__tests__/admin-panel.service.spec.ts @@ -6,7 +6,7 @@ import axios from 'axios'; import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { User } from 'src/engine/core-modules/user/user.entity'; @@ -59,7 +59,7 @@ describe('AdminPanelService', () => { }, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { getWorkspaceUrls: jest.fn().mockReturnValue({ customUrl: undefined, diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts index dc3ad9d375..85d61ef100 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.module.ts @@ -8,7 +8,7 @@ import { AdminPanelResolver } from 'src/engine/core-modules/admin-panel/admin-pa import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { HealthModule } from 'src/engine/core-modules/health/health.module'; @@ -22,8 +22,8 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi imports: [ TypeOrmModule.forFeature([User]), AuthModule, - DomainManagerModule, FileModule, + WorkspaceDomainsModule, HealthModule, RedisClientModule, TerminusModule, diff --git a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts index 413cd7c277..b0c7632b3e 100644 --- a/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts +++ b/packages/twenty-server/src/engine/core-modules/admin-panel/admin-panel.service.ts @@ -12,13 +12,11 @@ import { type ConfigVariablesGroupDataDTO } from 'src/engine/core-modules/admin- import { type ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output'; import { type UserLookup } from 'src/engine/core-modules/admin-panel/dtos/user-lookup.entity'; import { type VersionInfoDTO } from 'src/engine/core-modules/admin-panel/dtos/version-info.dto'; -import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { type FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; @@ -32,10 +30,8 @@ import { userValidator } from 'src/engine/core-modules/user/user.validate'; @Injectable() export class AdminPanelService { constructor( - private readonly loginTokenService: LoginTokenService, private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, - private readonly auditService: AuditService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly fileService: FileService, @InjectRepository(User) private readonly userRepository: Repository, @@ -90,7 +86,7 @@ export class AdminPanelService { }) : userWorkspace.workspace.logo, allowImpersonation: userWorkspace.workspace.allowImpersonation, - workspaceUrls: this.domainManagerService.getWorkspaceUrls({ + workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls({ subdomain: userWorkspace.workspace.subdomain, customDomain: userWorkspace.workspace.customDomain, isCustomDomainEnabled: userWorkspace.workspace.isCustomDomainEnabled, diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.module.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.module.ts index bd1ef25142..2e97bbd104 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.module.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/approved-access-domain.module.ts @@ -5,12 +5,12 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { ApprovedAccessDomain } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.entity'; import { ApprovedAccessDomainResolver } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.resolver'; import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @Module({ imports: [ - DomainManagerModule, + WorkspaceDomainsModule, FileModule, NestjsQueryTypeOrmModule.forFeature([ApprovedAccessDomain]), ], diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts index 430863ff57..6986f063bb 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.service.ts @@ -16,7 +16,7 @@ import { ApprovedAccessDomainExceptionCode, } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception'; import { approvedAccessDomainValidator } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.validate'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -31,8 +31,8 @@ export class ApprovedAccessDomainService { private readonly approvedAccessDomainRepository: Repository, private readonly emailService: EmailService, private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, private readonly fileService: FileService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} async sendApprovedAccessDomainValidationEmail( @@ -61,7 +61,7 @@ export class ApprovedAccessDomainService { ); } - const link = this.domainManagerService.buildWorkspaceURL({ + const link = this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname: getSettingsPath(SettingsPath.Domains), searchParams: { diff --git a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts index 90c45d2445..b544701ccb 100644 --- a/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/approved-access-domain/services/approved-access-domain.spec.ts @@ -10,7 +10,7 @@ import { ApprovedAccessDomainException, ApprovedAccessDomainExceptionCode, } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -35,7 +35,7 @@ describe('ApprovedAccessDomainService', () => { let approvedAccessDomainRepository: Repository; let emailService: EmailService; let twentyConfigService: TwentyConfigService; - let domainManagerService: DomainManagerService; + let workspaceDomainsService: WorkspaceDomainsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -63,7 +63,7 @@ describe('ApprovedAccessDomainService', () => { }, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { buildWorkspaceURL: jest.fn(), }, @@ -87,8 +87,9 @@ describe('ApprovedAccessDomainService', () => { ); emailService = module.get(EmailService); twentyConfigService = module.get(TwentyConfigService); - domainManagerService = - module.get(DomainManagerService); + workspaceDomainsService = module.get( + WorkspaceDomainsService, + ); }); describe('createApprovedAccessDomain', () => { @@ -286,7 +287,7 @@ describe('ApprovedAccessDomainService', () => { .mockResolvedValue(approvedAccessDomain); jest - .spyOn(domainManagerService, 'buildWorkspaceURL') + .spyOn(workspaceDomainsService, 'buildWorkspaceURL') .mockReturnValue(new URL('https://sub.twenty.com')); jest @@ -303,7 +304,7 @@ describe('ApprovedAccessDomainService', () => { approvedAccessDomain, ); - expect(domainManagerService.buildWorkspaceURL).toHaveBeenCalledWith({ + expect(workspaceDomainsService.buildWorkspaceURL).toHaveBeenCalledWith({ workspace: workspace, pathname: getSettingsPath(SettingsPath.Domains), searchParams: { validationToken: expect.any(String) }, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts index e08ebe8c9a..4978cf713d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.module.ts @@ -33,7 +33,9 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; +import { SubdomainManagerModule } from 'src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { EmailVerificationModule } from 'src/engine/core-modules/email-verification/email-verification.module'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; @@ -74,7 +76,7 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; JwtModule, FileUploadModule, DataSourceModule, - DomainManagerModule, + WorkspaceDomainsModule, TokenModule, UserModule, WorkspaceManagerModule, @@ -109,6 +111,8 @@ import { JwtAuthStrategy } from './strategies/jwt.auth.strategy'; TwoFactorAuthenticationModule, ApiKeyModule, AuditModule, + SubdomainManagerModule, + DomainServerConfigModule, ], controllers: [ GoogleAuthController, diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts index 39b826e1a5..954548308c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.spec.ts @@ -9,7 +9,7 @@ import { SignInUpService } from 'src/engine/core-modules/auth/services/sign-in-u import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service'; import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; @@ -64,7 +64,7 @@ describe('AuthResolver', () => { useValue: {}, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { buildWorkspaceURL: jest .fn() diff --git a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts index 50a5b84d9b..d41a16daff 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/auth.resolver.ts @@ -49,7 +49,7 @@ import { } from 'src/engine/core-modules/auth/types/auth-context.type'; import { CaptchaGuard } from 'src/engine/core-modules/captcha/captcha.guard'; import { CaptchaGraphqlApiExceptionFilter } from 'src/engine/core-modules/captcha/filters/captcha-graphql-api-exception.filter'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter'; @@ -61,6 +61,7 @@ import { TwoFactorAuthenticationExceptionFilter } from 'src/engine/core-modules/ import { TwoFactorAuthenticationService } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -74,7 +75,6 @@ import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard'; import { PermissionFlagType } from 'src/engine/metadata-modules/permissions/constants/permission-flag-type.constants'; import { PermissionsService } from 'src/engine/metadata-modules/permissions/permissions.service'; import { PermissionsGraphqlApiExceptionFilter } from 'src/engine/metadata-modules/permissions/utils/permissions-graphql-api-exception.filter'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { GetAuthTokensFromLoginTokenInput } from './dto/get-auth-tokens-from-login-token.input'; import { LoginToken } from './dto/login-token.entity'; @@ -115,8 +115,7 @@ export class AuthResolver { private signInUpService: SignInUpService, private transientTokenService: TransientTokenService, private emailVerificationService: EmailVerificationService, - // private oauthService: OAuthService, - private domainManagerService: DomainManagerService, + private workspaceDomainsService: WorkspaceDomainsService, private userWorkspaceService: UserWorkspaceService, private emailVerificationTokenService: EmailVerificationTokenService, private sSOService: SSOService, @@ -173,7 +172,7 @@ export class AuthResolver { @Args('origin') origin: string, ): Promise { const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, ); @@ -252,7 +251,7 @@ export class AuthResolver { ); const workspace = - (await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + (await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, )) ?? (await this.userWorkspaceService.findFirstWorkspaceByUserId( @@ -268,7 +267,8 @@ export class AuthResolver { authProvider, ); - const workspaceUrls = this.domainManagerService.getWorkspaceUrls(workspace); + const workspaceUrls = + this.workspaceDomainsService.getWorkspaceUrls(workspace); return { loginToken, workspaceUrls }; } @@ -330,7 +330,7 @@ export class AuthResolver { ); const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, ); @@ -475,7 +475,7 @@ export class AuthResolver { loginToken, workspace: { id: workspace.id, - workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace), + workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace), }, }; } @@ -504,20 +504,11 @@ export class AuthResolver { loginToken, workspace: { id: workspace.id, - workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace), + workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace), }, }; } - // @Mutation(() => ExchangeAuthCode) - // async exchangeAuthorizationCode( - // @Args() exchangeAuthCodeInput: ExchangeAuthCodeInput, - // ) { - // return await this.oauthService.verifyAuthorizationCode( - // exchangeAuthCodeInput, - // ); - // } - @Mutation(() => TransientToken) @UseGuards(UserAuthGuard) async generateTransientToken( @@ -606,7 +597,7 @@ export class AuthResolver { tokenWorkspaceId: string, ): Promise { const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts index 342cf184a0..56fa13099d 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/google-apis-auth.controller.ts @@ -23,12 +23,12 @@ import { GoogleAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth/gu import { GoogleAPIsService } from 'src/engine/core-modules/auth/services/google-apis.service'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { GoogleAPIsRequest } from 'src/engine/core-modules/auth/types/google-api-request.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; @Controller('auth/google-apis') @UseFilters(AuthRestApiExceptionFilter) @@ -38,7 +38,7 @@ export class GoogleAPIsAuthController { private readonly transientTokenService: TransientTokenService, private readonly twentyConfigService: TwentyConfigService, private readonly onboardingService: OnboardingService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, @@ -120,7 +120,7 @@ export class GoogleAPIsAuthController { connectedAccountId, }); - const url = this.domainManagerService.buildWorkspaceURL({ + const url = this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname, }); diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts index df78c1a998..45b50feba3 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/microsoft-apis-auth.controller.ts @@ -23,7 +23,7 @@ import { MicrosoftAPIsOauthRequestCodeGuard } from 'src/engine/core-modules/auth import { MicrosoftAPIsService } from 'src/engine/core-modules/auth/services/microsoft-apis.service'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { MicrosoftAPIsRequest } from 'src/engine/core-modules/auth/types/microsoft-api-request.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -37,7 +37,7 @@ export class MicrosoftAPIsAuthController { private readonly microsoftAPIsService: MicrosoftAPIsService, private readonly transientTokenService: TransientTokenService, private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly onboardingService: OnboardingService, private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace) @@ -127,7 +127,7 @@ export class MicrosoftAPIsAuthController { connectedAccountId, }); - const url = this.domainManagerService.buildWorkspaceURL({ + const url = this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname, }); diff --git a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts index d8d81741a6..b744836088 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/controllers/sso-auth.controller.ts @@ -14,8 +14,8 @@ import { InjectRepository } from '@nestjs/typeorm'; import { generateServiceProviderMetadata } from '@node-saml/node-saml'; import { Response } from 'express'; import { AppPath } from 'twenty-shared/types'; -import { Repository } from 'typeorm'; import { assertIsDefinedOrThrow } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; import { AuthException, @@ -29,17 +29,17 @@ import { AuthService } from 'src/engine/core-modules/auth/services/auth.service' import { OIDCRequest } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; import { SAMLRequest } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { IdentityProviderType, WorkspaceSSOIdentityProvider, } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type'; import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; @Controller('auth') @UseFilters(AuthRestApiExceptionFilter) @@ -48,7 +48,7 @@ export class SSOAuthController { private readonly loginTokenService: LoginTokenService, private readonly authService: AuthService, private readonly guardRedirectService: GuardRedirectService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly userService: UserService, private readonly sSOService: SSOService, @InjectRepository(WorkspaceSSOIdentityProvider) @@ -158,7 +158,7 @@ export class SSOAuthController { this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({ error, workspace: - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( workspaceIdentityProvider?.workspace, ), pathname: AppPath.Verify, diff --git a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts index 01a390c1b0..e3d11669e1 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/filters/auth-oauth-exception.filter.ts @@ -10,13 +10,13 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { HttpExceptionHandlerService } from 'src/engine/core-modules/exception-handler/http-exception-handler.service'; @Catch(AuthException) export class AuthOAuthExceptionFilter implements ExceptionFilter { constructor( - private readonly domainManagerService: DomainManagerService, + private readonly domainServerConfigService: DomainServerConfigService, private readonly httpExceptionHandlerService: HttpExceptionHandlerService, ) {} @@ -28,7 +28,7 @@ export class AuthOAuthExceptionFilter implements ExceptionFilter { case AuthExceptionCode.OAUTH_ACCESS_DENIED: response .status(403) - .redirect(this.domainManagerService.getBaseUrl().toString()); + .redirect(this.domainServerConfigService.getBaseUrl().toString()); break; default: return this.httpExceptionHandlerService.handleError( diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts index 31a7d52aa7..4a8aafac2a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-exchange-code-for-token.guard.ts @@ -11,7 +11,7 @@ import { import { GoogleAPIsOauthExchangeCodeForTokenStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-exchange-code-for-token.auth.strategy'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -24,9 +24,9 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( private readonly guardRedirectService: GuardRedirectService, private readonly twentyConfigService: TwentyConfigService, private readonly transientTokenService: TransientTokenService, - private readonly domainManagerService: DomainManagerService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super(); } @@ -66,7 +66,7 @@ export class GoogleAPIsOauthExchangeCodeForTokenGuard extends AuthGuard( ); const redirectErrorUrl = - this.domainManagerService.computeRedirectErrorUrl( + this.workspaceDomainsService.computeWorkspaceRedirectErrorUrl( 'We cannot connect to your Google account, please try again with more permissions, or a valid account', { subdomain: workspace.subdomain, diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts index 3b682c10fa..2737b7a2b6 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-apis-oauth-request-code.guard.ts @@ -11,7 +11,7 @@ import { import { GoogleAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/google-apis-oauth-request-code.auth.strategy'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -24,7 +24,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super({ prompt: 'select_account', @@ -73,7 +73,7 @@ export class GoogleAPIsOauthRequestCodeGuard extends AuthGuard('google-apis') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( workspace, ), ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts index c86e3316ca..e497ae84ba 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/google-oauth.guard.ts @@ -2,16 +2,16 @@ import { type ExecutionContext, Injectable } from '@nestjs/common'; import { AuthGuard } from '@nestjs/passport'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { type Request } from 'express'; +import { Repository } from 'typeorm'; import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Injectable() export class GoogleOauthGuard extends AuthGuard('google') { @@ -19,7 +19,7 @@ export class GoogleOauthGuard extends AuthGuard('google') { private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super({ prompt: 'select_account', @@ -53,7 +53,7 @@ export class GoogleOauthGuard extends AuthGuard('google') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( workspace, ), ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts index 657cf9d8fe..b4dac7a0f3 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-apis-oauth-request-code.guard.ts @@ -11,10 +11,10 @@ import { import { MicrosoftAPIsOauthRequestCodeStrategy } from 'src/engine/core-modules/auth/strategies/microsoft-apis-oauth-request-code.auth.strategy'; import { TransientTokenService } from 'src/engine/core-modules/auth/token/services/transient-token.service'; import { setRequestExtraParams } from 'src/engine/core-modules/auth/utils/google-apis-set-request-extra-params.util'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; @Injectable() export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( @@ -26,7 +26,7 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super({ prompt: 'select_account', @@ -72,7 +72,7 @@ export class MicrosoftAPIsOauthRequestCodeGuard extends AuthGuard( this.guardRedirectService.dispatchErrorFromGuard( context, err, - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( workspace, ), ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts index 28b86c88dc..ce28f86773 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/microsoft-oauth.guard.ts @@ -4,9 +4,9 @@ import { InjectRepository } from '@nestjs/typeorm'; import { Repository } from 'typeorm'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Injectable() export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { @@ -14,7 +14,7 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { private readonly guardRedirectService: GuardRedirectService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super({ prompt: 'select_account', @@ -41,7 +41,7 @@ export class MicrosoftOAuthGuard extends AuthGuard('microsoft') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( workspace, ), ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts index 246ff90811..b561aa4d34 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.guard.ts @@ -10,18 +10,18 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { OIDCAuthStrategy } from 'src/engine/core-modules/auth/strategies/oidc.auth.strategy'; -import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { type SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; import { type WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Injectable() export class OIDCAuthGuard extends AuthGuard('openidconnect') { constructor( private readonly sSOService: SSOService, private readonly guardRedirectService: GuardRedirectService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super(); } @@ -91,7 +91,7 @@ export class OIDCAuthGuard extends AuthGuard('openidconnect') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( identityProvider?.workspace, ), ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts index c6649bedb9..503718e8bd 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/oidc-auth.spec.ts @@ -1,15 +1,15 @@ import { type ExecutionContext } from '@nestjs/common'; -import { Test, type TestingModule } from '@nestjs/testing'; import { AuthGuard } from '@nestjs/passport'; +import { Test, type TestingModule } from '@nestjs/testing'; import { type Issuer } from 'openid-client'; -import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; -import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { OIDCAuthGuard } from 'src/engine/core-modules/auth/guards/oidc-auth.guard'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { type SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; import { type WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; const createMockExecutionContext = (mockedRequest: any): ExecutionContext => { return { @@ -60,7 +60,7 @@ describe('OIDCAuthGuard', () => { }, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain: jest.fn(), diff --git a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts index d572c13742..b6af601a45 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/guards/saml-auth.guard.ts @@ -10,18 +10,18 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { SamlAuthStrategy } from 'src/engine/core-modules/auth/strategies/saml.auth.strategy'; -import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; +import { SSOService } from 'src/engine/core-modules/sso/services/sso.service'; import { type SSOConfiguration } from 'src/engine/core-modules/sso/types/SSOConfigurations.type'; import { type WorkspaceSSOIdentityProvider } from 'src/engine/core-modules/sso/workspace-sso-identity-provider.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; @Injectable() export class SAMLAuthGuard extends AuthGuard('saml') { constructor( private readonly sSOService: SSOService, private readonly guardRedirectService: GuardRedirectService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) { super(); } @@ -51,7 +51,7 @@ export class SAMLAuthGuard extends AuthGuard('saml') { this.guardRedirectService.dispatchErrorFromGuard( context, err, - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( identityProvider?.workspace, ), ); diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts index fb2aa44847..c040e1baec 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.spec.ts @@ -17,7 +17,8 @@ import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/l import { RefreshTokenService } from 'src/engine/core-modules/auth/token/services/refresh-token.service'; import { WorkspaceAgnosticTokenService } from 'src/engine/core-modules/auth/token/services/workspace-agnostic-token.service'; import { type ExistingUserOrNewUser } from 'src/engine/core-modules/auth/types/signInUp.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -76,7 +77,11 @@ describe('AuthService', () => { useValue: {}, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, + useValue: {}, + }, + { + provide: DomainServerConfigService, useValue: {}, }, { diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts index d461fcf2af..4a8232d370 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/auth.service.ts @@ -50,19 +50,20 @@ import { type SignInUpBaseParams, type SignInUpNewUserPayload, } from 'src/engine/core-modules/auth/types/signInUp.type'; -import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { WorkspaceInvitationService } from 'src/engine/core-modules/workspace-invitation/services/workspace-invitation.service'; import { AuthProviderEnum } from 'src/engine/core-modules/workspace/types/workspace.type'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; @Injectable() // eslint-disable-next-line @nx/workspace-inject-workspace-repository @@ -70,7 +71,8 @@ export class AuthService { constructor( private readonly accessTokenService: AccessTokenService, private readonly workspaceAgnosticTokenService: WorkspaceAgnosticTokenService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, + private readonly domainServerConfigService: DomainServerConfigService, private readonly refreshTokenService: RefreshTokenService, private readonly loginTokenService: LoginTokenService, private readonly guardRedirectService: GuardRedirectService, @@ -535,7 +537,7 @@ export class AuthService { const emailTemplate = PasswordUpdateNotifyEmail({ userName: `${user.firstName} ${user.lastName}`, email: user.email, - link: this.domainManagerService.getBaseUrl().toString(), + link: this.domainServerConfigService.getBaseUrl().toString(), locale: firstUserWorkspace.locale, }); @@ -582,10 +584,10 @@ export class AuthService { billingCheckoutSessionState, }: { loginToken: string; - workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType; + workspace: WorkspaceDomainConfig; billingCheckoutSessionState?: string; }) { - const url = this.domainManagerService.buildWorkspaceURL({ + const url = this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname: AppPath.Verify, searchParams: { @@ -796,7 +798,7 @@ export class AuthService { }, )); - const url = this.domainManagerService.buildBaseUrl({ + const url = this.domainServerConfigService.buildBaseUrl({ pathname: AppPath.SignInUp, searchParams: { tokenPair: JSON.stringify({ @@ -882,7 +884,7 @@ export class AuthService { return this.guardRedirectService.getRedirectErrorUrlAndCaptureExceptions({ error, workspace: - this.domainManagerService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( + this.workspaceDomainsService.getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( currentWorkspace, ), pathname: AppPath.Verify, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts index 7561766c5b..271c34909a 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.spec.ts @@ -12,13 +12,14 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; +import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { type User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { ResetPasswordService } from './reset-password.service'; @@ -40,7 +41,7 @@ describe('ResetPasswordService', () => { let appTokenRepository: Repository; let emailService: EmailService; let twentyConfigService: TwentyConfigService; - let domainManagerService: DomainManagerService; + let workspaceDomainsService: WorkspaceDomainsService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -72,11 +73,16 @@ describe('ResetPasswordService', () => { }, }, { - provide: DomainManagerService, + provide: DomainServerConfigService, useValue: { getBaseUrl: jest .fn() .mockResolvedValue(new URL('http://localhost:3001')), + }, + }, + { + provide: WorkspaceDomainsService, + useValue: { buildWorkspaceURL: jest.fn(), }, }, @@ -107,8 +113,10 @@ describe('ResetPasswordService', () => { ); emailService = module.get(EmailService); twentyConfigService = module.get(TwentyConfigService); - domainManagerService = - module.get(DomainManagerService); + + workspaceDomainsService = module.get( + WorkspaceDomainsService, + ); }); it('should be defined', () => { @@ -197,7 +205,7 @@ describe('ResetPasswordService', () => { .spyOn(twentyConfigService, 'get') .mockReturnValue('http://localhost:3000'); jest - .spyOn(domainManagerService, 'buildWorkspaceURL') + .spyOn(workspaceDomainsService, 'buildWorkspaceURL') .mockReturnValue( new URL( 'https://subdomain.localhost.com:3000/reset-password/passwordResetToken', diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts index 0c0d8deb84..ad8c5e444c 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/reset-password.service.ts @@ -25,19 +25,19 @@ import { type EmailPasswordResetLink } from 'src/engine/core-modules/auth/dto/em import { type InvalidatePassword } from 'src/engine/core-modules/auth/dto/invalidate-password.entity'; import { type PasswordResetToken } from 'src/engine/core-modules/auth/dto/token.entity'; import { type ValidatePasswordResetToken } from 'src/engine/core-modules/auth/dto/validate-password-reset-token.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; -import { UserService } from 'src/engine/core-modules/user/services/user.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; @Injectable() export class ResetPasswordService { constructor( private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, @InjectRepository(AppToken) @@ -127,7 +127,7 @@ export class ResetPasswordService { assertIsDefinedOrThrow(workspace, WorkspaceNotFoundDefaultError); - const link = this.domainManagerService.buildWorkspaceURL({ + const link = this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname: getAppPath(AppPath.ResetPassword, { passwordResetToken: resetToken.passwordResetToken, diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts index 21e31e0f95..7ff20baa73 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.spec.ts @@ -17,7 +17,7 @@ import { type ExistingUserOrPartialUserWithPicture, type SignInUpBaseParams, } from 'src/engine/core-modules/auth/types/signInUp.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; @@ -45,7 +45,7 @@ describe('SignInUpService', () => { let workspaceInvitationService: WorkspaceInvitationService; let userWorkspaceService: UserWorkspaceService; let twentyConfigService: TwentyConfigService; - let domainManagerService: DomainManagerService; + let subdomainManagerService: SubdomainManagerService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -120,7 +120,7 @@ describe('SignInUpService', () => { }, }, { - provide: DomainManagerService, + provide: SubdomainManagerService, useValue: { generateSubdomain: jest.fn(), }, @@ -155,8 +155,9 @@ describe('SignInUpService', () => { userWorkspaceService = module.get(UserWorkspaceService); twentyConfigService = module.get(TwentyConfigService); - domainManagerService = - module.get(DomainManagerService); + subdomainManagerService = module.get( + SubdomainManagerService, + ); }); it('should handle signInUp with valid personal invitation', async () => { @@ -271,7 +272,7 @@ describe('SignInUpService', () => { } as Workspace); jest.spyOn(UserRepository, 'create').mockReturnValue({} as User); jest - .spyOn(domainManagerService, 'generateSubdomain') + .spyOn(subdomainManagerService, 'generateSubdomain') .mockResolvedValue('a-subdomain'); jest .spyOn(UserRepository, 'save') diff --git a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts index fff7f4bdef..6ea595bc84 100644 --- a/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts +++ b/packages/twenty-server/src/engine/core-modules/auth/services/sign-in-up.service.ts @@ -26,7 +26,7 @@ import { type SignInUpBaseParams, type SignInUpNewUserPayload, } from 'src/engine/core-modules/auth/types/signInUp.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service'; import { OnboardingService } from 'src/engine/core-modules/onboarding/onboarding.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -53,7 +53,7 @@ export class SignInUpService { private readonly workspaceEventEmitter: WorkspaceEventEmitter, private readonly httpService: HttpService, private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, + private readonly subdomainManagerService: SubdomainManagerService, private readonly userService: UserService, ) {} @@ -423,8 +423,8 @@ export class SignInUpService { isWorkEmailFound && (await isLogoUrlValid()) ? logoUrl : undefined; const workspaceToCreate = this.workspaceRepository.create({ - subdomain: await this.domainManagerService.generateSubdomain( - isWorkEmailFound ? { email } : {}, + subdomain: await this.subdomainManagerService.generateSubdomain( + isWorkEmailFound ? { userEmail: email } : {}, ), displayName: '', inviteHash: v4(), diff --git a/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts b/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts index ae6746b78b..7ef313cf66 100644 --- a/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing-webhook/billing-webhook.module.ts @@ -8,8 +8,8 @@ import { BillingWebhookEntitlementService } from 'src/engine/core-modules/billin import { BillingWebhookInvoiceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-invoice.service'; import { BillingWebhookPriceService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-price.service'; import { BillingWebhookProductService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-product.service'; -import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service'; import { BillingWebhookSubscriptionScheduleService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription-schedule.service'; +import { BillingWebhookSubscriptionService } from 'src/engine/core-modules/billing-webhook/services/billing-webhook-subscription.service'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingEntitlement } from 'src/engine/core-modules/billing/entities/billing-entitlement.entity'; @@ -19,7 +19,6 @@ import { BillingProduct } from 'src/engine/core-modules/billing/entities/billing import { BillingSubscriptionItem } from 'src/engine/core-modules/billing/entities/billing-subscription-item.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; @@ -32,7 +31,6 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi imports: [ FeatureFlagModule, StripeModule, - DomainManagerModule, MessageQueueModule, PermissionsModule, WorkspaceModule, diff --git a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts index 60067dd3c5..8e68f0ef50 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/billing.module.ts @@ -20,30 +20,30 @@ import { BillingFeatureUsedListener } from 'src/engine/core-modules/billing/list import { BillingWorkspaceMemberListener } from 'src/engine/core-modules/billing/listeners/billing-workspace-member.listener'; import { BillingPlanService } from 'src/engine/core-modules/billing/services/billing-plan.service'; import { BillingPortalWorkspaceService } from 'src/engine/core-modules/billing/services/billing-portal.workspace-service'; +import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service'; import { BillingProductService } from 'src/engine/core-modules/billing/services/billing-product.service'; import { BillingSubscriptionItemService } from 'src/engine/core-modules/billing/services/billing-subscription-item.service'; +import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingUsageService } from 'src/engine/core-modules/billing/services/billing-usage.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { StripeModule } from 'src/engine/core-modules/billing/stripe/stripe.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { MessageQueueModule } from 'src/engine/core-modules/message-queue/message-queue.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permissions.module'; -import { BillingSubscriptionPhaseService } from 'src/engine/core-modules/billing/services/billing-subscription-phase.service'; -import { BillingPriceService } from 'src/engine/core-modules/billing/services/billing-price.service'; @Module({ imports: [ FeatureFlagModule, StripeModule, - DomainManagerModule, MessageQueueModule, PermissionsModule, AiModule, + WorkspaceDomainsModule, TypeOrmModule.forFeature([ BillingSubscription, BillingSubscriptionItem, diff --git a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts index acfbdee7eb..7d675c5444 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/services/billing-portal.workspace-service.ts @@ -12,21 +12,21 @@ import { BillingException, BillingExceptionCode, } from 'src/engine/core-modules/billing/billing.exception'; +import { billingValidator } from 'src/engine/core-modules/billing/billing.validate'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; +import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum'; +import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service'; import { StripeCheckoutService } from 'src/engine/core-modules/billing/stripe/services/stripe-checkout.service'; import { type BillingGetPricesPerPlanResult } from 'src/engine/core-modules/billing/types/billing-get-prices-per-plan-result.type'; +import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type'; import { type BillingPortalCheckoutSessionParameters } from 'src/engine/core-modules/billing/types/billing-portal-checkout-session-parameters.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { assert } from 'src/utils/assert'; -import { billingValidator } from 'src/engine/core-modules/billing/billing.validate'; -import { BillingMeterPrice } from 'src/engine/core-modules/billing/types/billing-meter-price.type'; -import { BillingProductKey } from 'src/engine/core-modules/billing/enums/billing-product-key.enum'; -import { SubscriptionStatus } from 'src/engine/core-modules/billing/enums/billing-subscription-status.enum'; @Injectable() export class BillingPortalWorkspaceService { @@ -34,7 +34,7 @@ export class BillingPortalWorkspaceService { constructor( private readonly stripeCheckoutService: StripeCheckoutService, private readonly stripeBillingPortalService: StripeBillingPortalService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly billingSubscriptionService: BillingSubscriptionService, @InjectRepository(BillingSubscription) private readonly billingSubscriptionRepository: Repository, @@ -127,7 +127,7 @@ export class BillingPortalWorkspaceService { billingPricesPerPlan: BillingGetPricesPerPlanResult; successUrlPath?: string; }) { - const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({ + const frontBaseUrl = this.workspaceDomainsService.buildWorkspaceURL({ workspace, }); const cancelUrl = frontBaseUrl.toString(); @@ -182,7 +182,7 @@ export class BillingPortalWorkspaceService { throw new Error('Error: missing stripeCustomerId'); } - const frontBaseUrl = this.domainManagerService.buildWorkspaceURL({ + const frontBaseUrl = this.workspaceDomainsService.buildWorkspaceURL({ workspace, }); diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts index e2b9be2dcc..e580f105ab 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service.ts @@ -5,7 +5,7 @@ import { Injectable, Logger } from '@nestjs/common'; import type Stripe from 'stripe'; import { StripeSDKService } from 'src/engine/core-modules/billing/stripe/stripe-sdk/services/stripe-sdk.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @Injectable() @@ -15,7 +15,7 @@ export class StripeBillingPortalService { constructor( private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, + private readonly domainServerConfigService: DomainServerConfigService, private readonly stripeSDKService: StripeSDKService, ) { if (!this.twentyConfigService.get('IS_BILLING_ENABLED')) { @@ -33,7 +33,7 @@ export class StripeBillingPortalService { return await this.stripe.billingPortal.sessions.create({ customer: stripeCustomerId, return_url: - returnUrl ?? this.domainManagerService.getBaseUrl().toString(), + returnUrl ?? this.domainServerConfigService.getBaseUrl().toString(), }); } } diff --git a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts index b7a978ea5e..5cf50c3849 100644 --- a/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts +++ b/packages/twenty-server/src/engine/core-modules/billing/stripe/stripe.module.ts @@ -4,6 +4,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { BillingCustomer } from 'src/engine/core-modules/billing/entities/billing-customer.entity'; +import { StripeBillingAlertService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-alert.service'; import { StripeBillingMeterEventService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter-event.service'; import { StripeBillingMeterService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-meter.service'; import { StripeBillingPortalService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-portal.service'; @@ -12,16 +13,15 @@ import { StripeCustomerService } from 'src/engine/core-modules/billing/stripe/se import { StripePriceService } from 'src/engine/core-modules/billing/stripe/services/stripe-price.service'; import { StripeProductService } from 'src/engine/core-modules/billing/stripe/services/stripe-product.service'; import { StripeSubscriptionItemService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-item.service'; -import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; import { StripeSubscriptionScheduleService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription-schedule.service'; +import { StripeSubscriptionService } from 'src/engine/core-modules/billing/stripe/services/stripe-subscription.service'; import { StripeWebhookService } from 'src/engine/core-modules/billing/stripe/services/stripe-webhook.service'; import { StripeSDKModule } from 'src/engine/core-modules/billing/stripe/stripe-sdk/stripe-sdk.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; -import { StripeBillingAlertService } from 'src/engine/core-modules/billing/stripe/services/stripe-billing-alert.service'; +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; @Module({ imports: [ - DomainManagerModule, + DomainServerConfigModule, StripeSDKModule, TypeOrmModule.forFeature([BillingCustomer]), ], diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts index a3c408214c..3488a00484 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.module.ts @@ -1,13 +1,13 @@ import { Module } from '@nestjs/common'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; import { ClientConfigController } from './client-config.controller'; import { ClientConfigService } from './services/client-config.service'; @Module({ - imports: [DomainManagerModule], + imports: [DomainServerConfigModule], controllers: [ClientConfigController], providers: [ClientConfigService], }) diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts index 22e3629ee7..4bd4a291c3 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.spec.ts @@ -6,14 +6,14 @@ import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/ import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces'; import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; describe('ClientConfigService', () => { let service: ClientConfigService; let twentyConfigService: TwentyConfigService; - let domainManagerService: DomainManagerService; + let domainServerConfigService: DomainServerConfigService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -26,7 +26,7 @@ describe('ClientConfigService', () => { }, }, { - provide: DomainManagerService, + provide: DomainServerConfigService, useValue: { getFrontUrl: jest.fn(), }, @@ -42,8 +42,9 @@ describe('ClientConfigService', () => { service = module.get(ClientConfigService); twentyConfigService = module.get(TwentyConfigService); - domainManagerService = - module.get(DomainManagerService); + domainServerConfigService = module.get( + DomainServerConfigService, + ); }); it('should be defined', () => { @@ -91,7 +92,7 @@ describe('ClientConfigService', () => { return mockValues[key]; }); - jest.spyOn(domainManagerService, 'getFrontUrl').mockReturnValue({ + jest.spyOn(domainServerConfigService, 'getFrontUrl').mockReturnValue({ hostname: 'app.twenty.com', } as URL); }); diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts index 0f170a3dc0..4065e3f858 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts @@ -15,7 +15,7 @@ import { type ClientAIModelConfig, type ClientConfig, } from 'src/engine/core-modules/client-config/client-config.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; @@ -23,7 +23,7 @@ import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twent export class ClientConfigService { constructor( private twentyConfigService: TwentyConfigService, - private domainManagerService: DomainManagerService, + private domainServerConfigService: DomainServerConfigService, private aiModelRegistryService: AiModelRegistryService, ) {} @@ -107,7 +107,7 @@ export class ClientConfigService { 'IS_EMAIL_VERIFICATION_REQUIRED', ), defaultSubdomain: this.twentyConfigService.get('DEFAULT_SUBDOMAIN'), - frontDomain: this.domainManagerService.getFrontUrl().hostname, + frontDomain: this.domainServerConfigService.getFrontUrl().hostname, debugMode: this.twentyConfigService.get('NODE_ENV') === NodeEnvironment.DEVELOPMENT, diff --git a/packages/twenty-server/src/engine/core-modules/cloudflare/cloudflare.module.ts b/packages/twenty-server/src/engine/core-modules/cloudflare/cloudflare.module.ts index 264ad12dcb..6ed3f23707 100644 --- a/packages/twenty-server/src/engine/core-modules/cloudflare/cloudflare.module.ts +++ b/packages/twenty-server/src/engine/core-modules/cloudflare/cloudflare.module.ts @@ -2,17 +2,19 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; -import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public-domain.module'; -import { WorkspaceModule } from 'src/engine/core-modules/workspace/workspace.module'; import { DnsCloudflareController } from 'src/engine/core-modules/cloudflare/controllers/dns-cloudflare.controller'; import { DnsCloudflareService } from 'src/engine/core-modules/cloudflare/services/dns-cloudflare.service'; +import { CustomDomainManagerModule } from 'src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; +import { PublicDomainModule } from 'src/engine/core-modules/public-domain/public-domain.module'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @Module({ imports: [ NestjsQueryTypeOrmModule.forFeature([PublicDomain, Workspace]), - WorkspaceModule, + WorkspaceDomainsModule, + CustomDomainManagerModule, PublicDomainModule, ], providers: [DnsCloudflareService], diff --git a/packages/twenty-server/src/engine/core-modules/cloudflare/services/dns-cloudflare.service.ts b/packages/twenty-server/src/engine/core-modules/cloudflare/services/dns-cloudflare.service.ts index fec5417a1a..c254d23706 100644 --- a/packages/twenty-server/src/engine/core-modules/cloudflare/services/dns-cloudflare.service.ts +++ b/packages/twenty-server/src/engine/core-modules/cloudflare/services/dns-cloudflare.service.ts @@ -2,22 +2,26 @@ import { Injectable } from '@nestjs/common'; import { isDefined } from 'twenty-shared/utils'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.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'; import { PublicDomainService } from 'src/engine/core-modules/public-domain/public-domain.service'; @Injectable() -// eslint-disable-next-line @nx/workspace-inject-workspace-repository export class DnsCloudflareService { constructor( - private readonly workspaceService: WorkspaceService, private readonly publicDomainService: PublicDomainService, + private readonly customDomainManagerService: CustomDomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} async checkHostname(hostname: string) { - const workspace = await this.workspaceService.findByCustomDomain(hostname); + const workspace = + await this.workspaceDomainsService.findByCustomDomain(hostname); if (isDefined(workspace)) { - await this.workspaceService.checkCustomDomainValidRecords(workspace); + await this.customDomainManagerService.checkCustomDomainValidRecords( + workspace, + ); } const publicDomain = await this.publicDomainService.findByDomain(hostname); diff --git a/packages/twenty-server/src/engine/core-modules/dns-manager/dns-manager.module.ts b/packages/twenty-server/src/engine/core-modules/dns-manager/dns-manager.module.ts index 11cbc620d2..8f9f265430 100644 --- a/packages/twenty-server/src/engine/core-modules/dns-manager/dns-manager.module.ts +++ b/packages/twenty-server/src/engine/core-modules/dns-manager/dns-manager.module.ts @@ -1,10 +1,9 @@ import { Module } from '@nestjs/common'; import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; - +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; @Module({ - imports: [DomainManagerModule], + imports: [DomainServerConfigModule], providers: [DnsManagerService], exports: [DnsManagerService], }) 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 9467dc661d..d83ba09b1f 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 @@ -6,18 +6,18 @@ import { type CustomHostnameCreateResponse } from 'cloudflare/resources/custom-h import { AuditContextMock } from 'test/utils/audit-context.mock'; import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DnsManagerException } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception'; +import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; -import { DnsManagerException } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception'; jest.mock('cloudflare'); describe('DnsManagerService', () => { let dnsManagerService: DnsManagerService; let twentyConfigService: TwentyConfigService; - let domainManagerService: DomainManagerService; + let domainServerConfigService: DomainServerConfigService; beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ @@ -36,7 +36,7 @@ describe('DnsManagerService', () => { }, }, { - provide: DomainManagerService, + provide: DomainServerConfigService, useValue: { getBaseUrl: jest.fn(), getPublicDomainUrl: jest.fn(), @@ -53,8 +53,9 @@ describe('DnsManagerService', () => { dnsManagerService = module.get(DnsManagerService); twentyConfigService = module.get(TwentyConfigService); - domainManagerService = - module.get(DomainManagerService); + domainServerConfigService = module.get( + DomainServerConfigService, + ); (dnsManagerService as any).cloudflareClient = { customHostnames: { @@ -165,7 +166,7 @@ describe('DnsManagerService', () => { jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id'); jest - .spyOn(domainManagerService, 'getBaseUrl') + .spyOn(domainServerConfigService, 'getBaseUrl') .mockReturnValue(new URL('https://front.domain')); (dnsManagerService as any).cloudflareClient = cloudflareMock; @@ -204,7 +205,7 @@ describe('DnsManagerService', () => { jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id'); jest - .spyOn(domainManagerService, 'getBaseUrl') + .spyOn(domainServerConfigService, 'getBaseUrl') .mockReturnValue(new URL('https://front.domain')); (dnsManagerService as any).cloudflareClient = cloudflareMock; @@ -245,11 +246,11 @@ describe('DnsManagerService', () => { jest.spyOn(twentyConfigService, 'get').mockReturnValue('test-zone-id'); jest - .spyOn(domainManagerService, 'getBaseUrl') + .spyOn(domainServerConfigService, 'getBaseUrl') .mockReturnValue(new URL('https://front.domain')); jest - .spyOn(domainManagerService, 'getPublicDomainUrl') + .spyOn(domainServerConfigService, 'getPublicDomainUrl') .mockReturnValue(new URL('https://front.public-domain')); (dnsManagerService as any).cloudflareClient = cloudflareMock; 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 0b54fdce55..d02693873b 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 @@ -15,7 +15,7 @@ import { DnsManagerExceptionCode, } from 'src/engine/core-modules/dns-manager/exceptions/dns-manager.exception'; import { dnsManagerValidator } from 'src/engine/core-modules/dns-manager/validator/dns-manager.validate'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; type DnsManagerOptions = { @@ -28,7 +28,7 @@ export class DnsManagerService { constructor( private readonly twentyConfigService: TwentyConfigService, - private readonly domainManagerService: DomainManagerService, + private readonly domainServerConfigService: DomainServerConfigService, ) { if (this.twentyConfigService.get('CLOUDFLARE_API_KEY')) { this.cloudflareClient = new Cloudflare({ @@ -61,7 +61,7 @@ export class DnsManagerService { ): Promise { if ( options?.isPublicDomain && - !isDefined(this.domainManagerService.getPublicDomainUrl().hostname) + !isDefined(this.domainServerConfigService.getPublicDomainUrl().hostname) ) { throw new DnsManagerException( 'Missing public domain URL', @@ -95,8 +95,8 @@ export class DnsManagerService { status: statuses.redirection, key: hostname, value: options?.isPublicDomain - ? this.domainManagerService.getPublicDomainUrl().hostname - : this.domainManagerService.getBaseUrl().hostname, + ? this.domainServerConfigService.getPublicDomainUrl().hostname + : this.domainServerConfigService.getBaseUrl().hostname, }, { validationType: 'ssl' as const, diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts b/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts deleted file mode 100644 index de33d44c15..0000000000 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.module.ts +++ /dev/null @@ -1,14 +0,0 @@ -import { Module } from '@nestjs/common'; -import { TypeOrmModule } from '@nestjs/typeorm'; - -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; -import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; -import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; - -@Module({ - imports: [TypeOrmModule.forFeature([Workspace, PublicDomain]), AuditModule], - providers: [DomainManagerService], - exports: [DomainManagerService], -}) -export class DomainManagerModule {} diff --git a/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module.ts b/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module.ts new file mode 100644 index 0000000000..6fc34537a0 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module.ts @@ -0,0 +1,21 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; +import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; +import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module'; +import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Module({ + imports: [ + TypeOrmModule.forFeature([Workspace, PublicDomain]), + DnsManagerModule, + BillingModule, + AuditModule, + ], + providers: [CustomDomainManagerService], + exports: [CustomDomainManagerService], +}) +export class CustomDomainManagerModule {} diff --git a/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts new file mode 100644 index 0000000000..6bd5de30a8 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service.ts @@ -0,0 +1,127 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { msg } from '@lingui/core/macro'; +import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; +import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated'; +import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated'; +import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; +import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; +import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records'; +import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + WorkspaceException, + WorkspaceExceptionCode, +} from 'src/engine/core-modules/workspace/workspace.exception'; + +@Injectable() +export class CustomDomainManagerService { + constructor( + @InjectRepository(Workspace) + private readonly workspaceRepository: Repository, + @InjectRepository(PublicDomain) + private readonly publicDomainRepository: Repository, + private readonly billingService: BillingService, + private readonly dnsManagerService: DnsManagerService, + private readonly auditService: AuditService, + ) {} + + async isCustomDomainEnabled(workspaceId: string) { + const isCustomDomainBillingEnabled = + await this.billingService.hasEntitlement( + workspaceId, + BillingEntitlementKey.CUSTOM_DOMAIN, + ); + + if (!isCustomDomainBillingEnabled) { + throw new WorkspaceException( + `No entitlement found for this workspace`, + WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED, + ); + } + } + + async setCustomDomain(workspace: Workspace, customDomain: string) { + await this.isCustomDomainEnabled(workspace.id); + + const existingWorkspace = await this.workspaceRepository.findOne({ + where: { customDomain }, + }); + + if (existingWorkspace && existingWorkspace.id !== workspace.id) { + throw new WorkspaceException( + 'Domain already taken', + WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN, + ); + } + + if ( + await this.publicDomainRepository.findOneBy({ + domain: customDomain, + }) + ) { + throw new WorkspaceException( + 'Domain is already registered as public domain', + WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN, + { + userFriendlyMessage: msg`Domain is already registered as public domain`, + }, + ); + } + + if (!isDefined(customDomain) || workspace.customDomain === customDomain) { + return; + } + + if (isDefined(workspace.customDomain)) { + await this.dnsManagerService.updateHostname( + workspace.customDomain, + customDomain, + ); + } else { + await this.dnsManagerService.registerHostname(customDomain); + } + } + + async checkCustomDomainValidRecords( + workspace: Workspace, + domainValidRecord?: DomainValidRecords, + ) { + assertIsDefinedOrThrow(workspace.customDomain); + + const customDomainWithRecords = + domainValidRecord ?? + (await this.dnsManagerService.getHostnameWithRecords( + workspace.customDomain, + )); + + assertIsDefinedOrThrow(customDomainWithRecords); + + const isCustomDomainWorking = + await this.dnsManagerService.isHostnameWorking(workspace.customDomain); + + if (workspace.isCustomDomainEnabled !== isCustomDomainWorking) { + workspace.isCustomDomainEnabled = isCustomDomainWorking; + + await this.workspaceRepository.save(workspace); + + const analytics = this.auditService.createContext({ + workspaceId: workspace.id, + }); + + analytics.insertWorkspaceEvent( + workspace.isCustomDomainEnabled + ? CUSTOM_DOMAIN_ACTIVATED_EVENT + : CUSTOM_DOMAIN_DEACTIVATED_EVENT, + {}, + ); + } + + return customDomainWithRecords; + } +} diff --git a/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/domain-server-config.module.ts b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/domain-server-config.module.ts new file mode 100644 index 0000000000..2b2d19cf61 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/domain-server-config.module.ts @@ -0,0 +1,10 @@ +import { Module } from '@nestjs/common'; + +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; + +@Module({ + imports: [], + providers: [DomainServerConfigService], + exports: [DomainServerConfigService], +}) +export class DomainServerConfigModule {} diff --git a/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/__test__/domain-server-config.service.spec.ts b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/__test__/domain-server-config.service.spec.ts new file mode 100644 index 0000000000..a17a175a1d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/__test__/domain-server-config.service.spec.ts @@ -0,0 +1,82 @@ +import { Test, type TestingModule } from '@nestjs/testing'; +import { getRepositoryToken } from '@nestjs/typeorm'; + +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +describe('SubdomainManagerService', () => { + let domainServerConfigService: DomainServerConfigService; + let twentyConfigService: TwentyConfigService; + + beforeEach(async () => { + const module: TestingModule = await Test.createTestingModule({ + providers: [ + DomainServerConfigService, + { + provide: getRepositoryToken(Workspace), + useValue: { + find: jest.fn(), + findOne: jest.fn(), + }, + }, + { + provide: getRepositoryToken(PublicDomain), + useValue: { + findOne: jest.fn(), + }, + }, + { + provide: TwentyConfigService, + useValue: { + get: jest.fn(), + }, + }, + ], + }).compile(); + + domainServerConfigService = module.get( + DomainServerConfigService, + ); + twentyConfigService = module.get(TwentyConfigService); + }); + + describe('buildBaseUrl', () => { + it('should build the base URL from environment variables', () => { + jest + .spyOn(twentyConfigService, 'get') + .mockImplementation((key: string) => { + const env = { + FRONTEND_URL: 'https://example.com', + }; + + // @ts-expect-error legacy noImplicitAny + return env[key]; + }); + + const result = domainServerConfigService.getBaseUrl(); + + expect(result.toString()).toBe('https://example.com/'); + }); + + it('should append default subdomain if multiworkspace is enabled', () => { + jest + .spyOn(twentyConfigService, 'get') + .mockImplementation((key: string) => { + const env = { + FRONTEND_URL: 'https://example.com', + IS_MULTIWORKSPACE_ENABLED: true, + DEFAULT_SUBDOMAIN: 'test', + }; + + // @ts-expect-error legacy noImplicitAny + return env[key]; + }); + + const result = domainServerConfigService.getBaseUrl(); + + expect(result.toString()).toBe('https://test.example.com/'); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service.ts b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service.ts new file mode 100644 index 0000000000..99d4d534c7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service.ts @@ -0,0 +1,69 @@ +import { Injectable } from '@nestjs/common'; + +import { buildUrlWithPathnameAndSearchParams } from 'src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; + +@Injectable() +export class DomainServerConfigService { + constructor(private readonly twentyConfigService: TwentyConfigService) {} + + getFrontUrl() { + return new URL( + this.twentyConfigService.get('FRONTEND_URL') ?? + this.twentyConfigService.get('SERVER_URL'), + ); + } + + getBaseUrl(): URL { + const baseUrl = this.getFrontUrl(); + + if ( + this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED') && + this.twentyConfigService.get('DEFAULT_SUBDOMAIN') + ) { + baseUrl.hostname = `${this.twentyConfigService.get('DEFAULT_SUBDOMAIN')}.${baseUrl.hostname}`; + } + + return baseUrl; + } + + getPublicDomainUrl(): URL { + return new URL(this.twentyConfigService.get('PUBLIC_DOMAIN_URL')); + } + + buildBaseUrl({ + pathname, + searchParams, + }: { + pathname?: string; + searchParams?: Record; + }) { + return buildUrlWithPathnameAndSearchParams({ + baseUrl: this.getBaseUrl(), + pathname, + searchParams, + }); + } + + getSubdomainAndDomainFromUrl = (url: string) => { + const { hostname: originHostname } = new URL(url); + + const frontDomain = this.getFrontUrl().hostname; + + const isFrontdomain = originHostname.endsWith(`.${frontDomain}`); + + const subdomain = originHostname.replace(`.${frontDomain}`, ''); + + return { + subdomain: + isFrontdomain && !this.isDefaultSubdomain(subdomain) + ? subdomain + : undefined, + domain: isFrontdomain ? null : originHostname, + }; + }; + + isDefaultSubdomain(subdomain: string) { + return subdomain === this.twentyConfigService.get('DEFAULT_SUBDOMAIN'); + } +} diff --git a/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/append-search-params-to-url.util.ts b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/append-search-params-to-url.util.ts new file mode 100644 index 0000000000..569533ede5 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/append-search-params-to-url.util.ts @@ -0,0 +1,8 @@ +export const appendSearchParamsToUrl = ( + url: URL, + searchParams: Record, +) => { + Object.entries(searchParams).forEach(([key, value]) => { + url.searchParams.set(key, value.toString()); + }); +}; diff --git a/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util.ts b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util.ts new file mode 100644 index 0000000000..bea0a0b890 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util.ts @@ -0,0 +1,25 @@ +import { appendSearchParamsToUrl } from 'src/engine/core-modules/domain/domain-server-config/utils/append-search-params-to-url.util'; + +type BuildUrlWithPathnameAndSearchParamsProps = { + baseUrl: URL; + pathname?: string; + searchParams?: Record; +}; + +export const buildUrlWithPathnameAndSearchParams = ({ + baseUrl, + pathname, + searchParams, +}: BuildUrlWithPathnameAndSearchParamsProps) => { + const url = baseUrl; + + if (pathname) { + url.pathname = pathname; + } + + if (searchParams) { + appendSearchParamsToUrl(url, searchParams); + } + + return url; +}; diff --git a/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service.ts new file mode 100644 index 0000000000..f700d6795d --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@nestjs/common'; +import { InjectRepository } from '@nestjs/typeorm'; + +import { isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; + +import { generateRandomSubdomain } from 'src/engine/core-modules/domain/subdomain-manager/utils/generate-random-subdomain.util'; +import { getSubdomainFromEmail } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util'; +import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util'; +import { isSubdomainValid } from 'src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util'; +import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; +import { + WorkspaceException, + WorkspaceExceptionCode, +} from 'src/engine/core-modules/workspace/workspace.exception'; + +@Injectable() +export class SubdomainManagerService { + constructor( + @InjectRepository(Workspace) + private readonly workspaceRepository: Repository, + private readonly twentyConfigService: TwentyConfigService, + ) {} + + async generateSubdomain({ + userEmail, + workspaceDisplayName, + }: { + userEmail?: string; + workspaceDisplayName?: string; + }) { + const subdomainFromUserEmail = getSubdomainFromEmail(userEmail); + const subdomainFromWorkspaceDisplayName = + getSubdomainNameFromDisplayName(workspaceDisplayName); + + const extractedSubdomain = + subdomainFromUserEmail || subdomainFromWorkspaceDisplayName; + + const isExtractedSubdomainValid = isDefined(extractedSubdomain) + ? isSubdomainValid(extractedSubdomain) + : false; + + const subdomain = isExtractedSubdomainValid + ? extractedSubdomain + : generateRandomSubdomain(); + + const existingWorkspaceCount = await this.workspaceRepository.countBy({ + subdomain, + }); + + return `${subdomain}${existingWorkspaceCount > 0 ? `-${Math.random().toString(36).substring(2, 10)}` : ''}`; + } + + async isSubdomainAvailable(subdomain: string) { + const existingWorkspace = await this.workspaceRepository.findOne({ + where: { subdomain: subdomain }, + }); + + return !existingWorkspace; + } + + async validateSubdomainOrThrow(subdomain: string) { + const isValid = isSubdomainValid(subdomain); + + if (!isValid) { + throw new WorkspaceException( + 'Subdomain is not valid', + WorkspaceExceptionCode.SUBDOMAIN_NOT_VALID, + ); + } + + const isAvailable = await this.isSubdomainAvailable(subdomain); + + if ( + !isAvailable || + this.twentyConfigService.get('DEFAULT_SUBDOMAIN') === subdomain + ) { + throw new WorkspaceException( + 'Subdomain already taken', + WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN, + ); + } + } +} diff --git a/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module.ts new file mode 100644 index 0000000000..b1491421dc --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module.ts @@ -0,0 +1,12 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Module({ + imports: [TypeOrmModule.forFeature([Workspace])], + providers: [SubdomainManagerService], + exports: [SubdomainManagerService], +}) +export class SubdomainManagerModule {} diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/generate-random-subdomain.spec.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/generate-random-subdomain.util.spec.ts similarity index 88% rename from packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/generate-random-subdomain.spec.ts rename to packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/generate-random-subdomain.util.spec.ts index ecbae2c164..54fd4a007e 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/generate-random-subdomain.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/generate-random-subdomain.util.spec.ts @@ -1,4 +1,4 @@ -import { generateRandomSubdomain } from 'src/engine/core-modules/domain-manager/utils/generate-random-subdomain'; +import { generateRandomSubdomain } from 'src/engine/core-modules/domain/subdomain-manager/utils/generate-random-subdomain.util'; describe('generateRandomSubdomain', () => { it('should return a string in the format "prefix-color-suffix"', () => { diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/get-subdomain-from-email.spec.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/get-subdomain-from-email.util.spec.ts similarity index 91% rename from packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/get-subdomain-from-email.spec.ts rename to packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/get-subdomain-from-email.util.spec.ts index 342c9dce12..2157b9a26c 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/get-subdomain-from-email.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/get-subdomain-from-email.util.spec.ts @@ -1,4 +1,4 @@ -import { getSubdomainFromEmail } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-from-email'; +import { getSubdomainFromEmail } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util'; describe('getSubdomainFromEmail', () => { beforeEach(() => { diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/get-subdomain-name-from-display-name.spec.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/get-subdomain-name-from-display-name.util.spec.ts similarity index 94% rename from packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/get-subdomain-name-from-display-name.spec.ts rename to packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/get-subdomain-name-from-display-name.util.spec.ts index 5cd28198e3..64ad4a9cce 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/__test__/get-subdomain-name-from-display-name.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/get-subdomain-name-from-display-name.util.spec.ts @@ -1,4 +1,4 @@ -import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name'; +import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util'; describe('getSubdomainNameFromDisplayName', () => { it('should return a hyphen-separated, lowercase subdomain name without spaces for a valid display name', () => { diff --git a/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/is-subdomain-valid.util.spec.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/is-subdomain-valid.util.spec.ts new file mode 100644 index 0000000000..a9eaaa548a --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/__test__/is-subdomain-valid.util.spec.ts @@ -0,0 +1,384 @@ +import { isSubdomainValid } from 'src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util'; + +describe('isSubdomainValid', () => { + describe('valid subdomains', () => { + it('should accept valid alphanumeric subdomains', () => { + expect(isSubdomainValid('abc')).toBe(true); + expect(isSubdomainValid('test123')).toBe(true); + expect(isSubdomainValid('company1')).toBe(true); + expect(isSubdomainValid('workspace2024')).toBe(true); + }); + + it('should accept subdomains with hyphens in the middle', () => { + expect(isSubdomainValid('my-company')).toBe(true); + expect(isSubdomainValid('test-workspace')).toBe(true); + expect(isSubdomainValid('multi-word-subdomain')).toBe(true); + expect(isSubdomainValid('a-b-c-d-e')).toBe(true); + }); + + it('should accept subdomains with mixed alphanumeric and hyphens', () => { + expect(isSubdomainValid('test-123')).toBe(true); + expect(isSubdomainValid('company-2024')).toBe(true); + expect(isSubdomainValid('workspace-v2')).toBe(true); + expect(isSubdomainValid('my-app-123')).toBe(true); + }); + + it('should accept minimum length subdomains (3 characters)', () => { + expect(isSubdomainValid('abc')).toBe(true); + expect(isSubdomainValid('a1b')).toBe(true); + expect(isSubdomainValid('x-y')).toBe(true); + }); + + it('should accept maximum length subdomains (30 characters)', () => { + const maxLengthSubdomain = 'a'.repeat(28) + 'bc'; // 30 characters total + + expect(isSubdomainValid(maxLengthSubdomain)).toBe(true); + + const maxLengthWithHyphens = 'a' + '-'.repeat(28) + 'b'; // 30 characters with hyphens + + expect(isSubdomainValid(maxLengthWithHyphens)).toBe(true); + }); + + it('should accept subdomains starting and ending with alphanumeric characters', () => { + expect(isSubdomainValid('a-b')).toBe(true); + expect(isSubdomainValid('1-test-2')).toBe(true); + expect(isSubdomainValid('start-middle-end')).toBe(true); + }); + }); + + describe('invalid subdomain patterns', () => { + it('should reject empty strings', () => { + expect(isSubdomainValid('')).toBe(false); + }); + + it('should reject subdomains that are too short (less than 3 characters)', () => { + expect(isSubdomainValid('a')).toBe(false); + expect(isSubdomainValid('ab')).toBe(false); + expect(isSubdomainValid('1')).toBe(false); + expect(isSubdomainValid('12')).toBe(false); + }); + + it('should reject subdomains that are too long (more than 30 characters)', () => { + const tooLongSubdomain = 'a'.repeat(31); + + expect(isSubdomainValid(tooLongSubdomain)).toBe(false); + + const wayTooLongSubdomain = 'a'.repeat(50); + + expect(isSubdomainValid(wayTooLongSubdomain)).toBe(false); + }); + + it('should reject subdomains starting with hyphens', () => { + expect(isSubdomainValid('-test')).toBe(false); + expect(isSubdomainValid('-abc')).toBe(false); + expect(isSubdomainValid('-my-company')).toBe(false); + }); + + it('should reject subdomains ending with hyphens', () => { + expect(isSubdomainValid('test-')).toBe(false); + expect(isSubdomainValid('abc-')).toBe(false); + expect(isSubdomainValid('my-company-')).toBe(false); + }); + + it('should reject subdomains with uppercase letters', () => { + expect(isSubdomainValid('Test')).toBe(false); + expect(isSubdomainValid('MyCompany')).toBe(false); + expect(isSubdomainValid('WORKSPACE')).toBe(false); + expect(isSubdomainValid('test-Company')).toBe(false); + }); + + it('should reject subdomains with special characters', () => { + expect(isSubdomainValid('test@company')).toBe(false); + expect(isSubdomainValid('my_workspace')).toBe(false); + expect(isSubdomainValid('test.company')).toBe(false); + expect(isSubdomainValid('workspace#1')).toBe(false); + expect(isSubdomainValid('test$company')).toBe(false); + expect(isSubdomainValid('my%workspace')).toBe(false); + expect(isSubdomainValid('test&company')).toBe(false); + expect(isSubdomainValid('workspace*1')).toBe(false); + expect(isSubdomainValid('test+company')).toBe(false); + expect(isSubdomainValid('my=workspace')).toBe(false); + }); + + it('should reject subdomains with spaces', () => { + expect(isSubdomainValid('test company')).toBe(false); + expect(isSubdomainValid('my workspace')).toBe(false); + expect(isSubdomainValid(' test')).toBe(false); + expect(isSubdomainValid('test ')).toBe(false); + expect(isSubdomainValid(' ')).toBe(false); + }); + + it('should reject subdomains starting with "api-"', () => { + expect(isSubdomainValid('api-test')).toBe(false); + expect(isSubdomainValid('api-company')).toBe(false); + expect(isSubdomainValid('api-workspace')).toBe(false); + expect(isSubdomainValid('api-123')).toBe(false); + }); + + it('should reject subdomains with only hyphens', () => { + expect(isSubdomainValid('---')).toBe(false); + expect(isSubdomainValid('----')).toBe(false); + }); + + it('should reject subdomains with numbers only at boundaries but invalid patterns', () => { + expect(isSubdomainValid('1-')).toBe(false); + expect(isSubdomainValid('-1')).toBe(false); + }); + }); + + describe('reserved subdomains', () => { + it('should reject common reserved subdomains', () => { + expect(isSubdomainValid('api')).toBe(false); + expect(isSubdomainValid('www')).toBe(false); + expect(isSubdomainValid('admin')).toBe(false); + expect(isSubdomainValid('dashboard')).toBe(false); + expect(isSubdomainValid('billing')).toBe(false); + expect(isSubdomainValid('support')).toBe(false); + }); + + it('should reject technical reserved subdomains', () => { + expect(isSubdomainValid('db')).toBe(false); + expect(isSubdomainValid('cdn')).toBe(false); + expect(isSubdomainValid('storage')).toBe(false); + expect(isSubdomainValid('files')).toBe(false); + expect(isSubdomainValid('media')).toBe(false); + expect(isSubdomainValid('assets')).toBe(false); + }); + + it('should reject authentication related reserved subdomains', () => { + expect(isSubdomainValid('auth')).toBe(false); + expect(isSubdomainValid('login')).toBe(false); + expect(isSubdomainValid('signin')).toBe(false); + expect(isSubdomainValid('signup')).toBe(false); + expect(isSubdomainValid('register')).toBe(false); + }); + + it('should reject business related reserved subdomains', () => { + expect(isSubdomainValid('about')).toBe(false); + expect(isSubdomainValid('contact')).toBe(false); + expect(isSubdomainValid('careers')).toBe(false); + expect(isSubdomainValid('jobs')).toBe(false); + expect(isSubdomainValid('blog')).toBe(false); + expect(isSubdomainValid('news')).toBe(false); + }); + + it('should reject country code reserved subdomains', () => { + expect(isSubdomainValid('us')).toBe(false); + expect(isSubdomainValid('uk')).toBe(false); + expect(isSubdomainValid('ca')).toBe(false); + expect(isSubdomainValid('au')).toBe(false); + expect(isSubdomainValid('de')).toBe(false); + expect(isSubdomainValid('fr')).toBe(false); + expect(isSubdomainValid('it')).toBe(false); + expect(isSubdomainValid('es')).toBe(false); + }); + + it('should reject geographic reserved subdomains', () => { + expect(isSubdomainValid('europe')).toBe(false); + expect(isSubdomainValid('asia')).toBe(false); + expect(isSubdomainValid('africa')).toBe(false); + expect(isSubdomainValid('america')).toBe(false); + expect(isSubdomainValid('oceania')).toBe(false); + expect(isSubdomainValid('paris')).toBe(false); + expect(isSubdomainValid('london')).toBe(false); + expect(isSubdomainValid('new-york')).toBe(false); + }); + + it('should reject environment related reserved subdomains', () => { + expect(isSubdomainValid('dev')).toBe(false); + expect(isSubdomainValid('test')).toBe(false); + expect(isSubdomainValid('testing')).toBe(false); + expect(isSubdomainValid('staging')).toBe(false); + expect(isSubdomainValid('production')).toBe(false); + }); + + it('should reject reserved subdomains case-insensitively', () => { + expect(isSubdomainValid('API')).toBe(false); + expect(isSubdomainValid('Api')).toBe(false); + expect(isSubdomainValid('WWW')).toBe(false); + expect(isSubdomainValid('Www')).toBe(false); + expect(isSubdomainValid('ADMIN')).toBe(false); + expect(isSubdomainValid('Admin')).toBe(false); + expect(isSubdomainValid('TEST')).toBe(false); + expect(isSubdomainValid('Test')).toBe(false); + }); + + it('should reject all reserved subdomains from the constant', () => { + const reservedSubdomains = [ + 'trust', + 'demo', + 'api', + 't', + 'companies', + 'telemetry', + 'logs', + 'metrics', + 'next', + 'main', + 'admin', + 'dashboard', + 'dash', + 'billing', + 'db', + 'favicon', + 'www', + 'mail', + 'docs', + 'dev', + 'app', + 'staging', + 'production', + 'developer', + 'files', + 'cdn', + 'storage', + 'about', + 'help', + 'support', + 'contact', + 'privacy', + 'terms', + 'careers', + 'jobs', + 'blog', + 'news', + 'events', + 'community', + 'forum', + 'chat', + 'test', + 'testing', + 'feedback', + 'config', + 'settings', + 'media', + 'image', + 'audio', + 'video', + 'images', + 'partners', + 'partnership', + 'partnerships', + 'assets', + 'login', + 'signin', + 'signup', + 'legal', + 'shop', + 'merch', + 'store', + 'auth', + 'register', + 'payment', + ]; + + reservedSubdomains.forEach((subdomain) => { + expect(isSubdomainValid(subdomain)).toBe(false); + }); + }); + }); + + describe('edge cases', () => { + it('should handle whitespace-only strings', () => { + expect(isSubdomainValid(' ')).toBe(false); + expect(isSubdomainValid('\t')).toBe(false); + expect(isSubdomainValid('\n')).toBe(false); + expect(isSubdomainValid('\r')).toBe(false); + }); + + it('should handle strings with leading/trailing whitespace', () => { + expect(isSubdomainValid(' test')).toBe(false); + expect(isSubdomainValid('test ')).toBe(false); + expect(isSubdomainValid(' test ')).toBe(false); + expect(isSubdomainValid('\ttest\t')).toBe(false); + }); + + it('should handle boundary length cases precisely', () => { + // Exactly 3 characters (minimum valid) + expect(isSubdomainValid('abc')).toBe(true); + + // Exactly 30 characters (maximum valid) + const exactly30Chars = 'a'.repeat(28) + 'bc'; + + expect(exactly30Chars.length).toBe(30); + expect(isSubdomainValid(exactly30Chars)).toBe(true); + + // Exactly 31 characters (first invalid length) + const exactly31Chars = 'a'.repeat(29) + 'bc'; + + expect(exactly31Chars.length).toBe(31); + expect(isSubdomainValid(exactly31Chars)).toBe(false); + }); + + it('should validate that reserved subdomains check is case insensitive', () => { + // Test mixed case variations of reserved subdomains + expect(isSubdomainValid('Trust')).toBe(false); + expect(isSubdomainValid('TRUST')).toBe(false); + expect(isSubdomainValid('tRuSt')).toBe(false); + expect(isSubdomainValid('Demo')).toBe(false); + expect(isSubdomainValid('DEMO')).toBe(false); + expect(isSubdomainValid('dEmO')).toBe(false); + }); + + it('should accept valid subdomains that are similar to reserved ones but not exact matches', () => { + // 'testing' is reserved, but 'testing123' is not + expect(isSubdomainValid('testing123')).toBe(true); + // 'api' is reserved, but 'myapi' is not + expect(isSubdomainValid('myapi')).toBe(true); + // 'admin' is reserved, but 'adminpanel' is not + expect(isSubdomainValid('adminpanel')).toBe(true); + // 'test' is reserved, but 'testapp' is not + expect(isSubdomainValid('testapp')).toBe(true); + }); + + it('should handle Unicode characters', () => { + expect(isSubdomainValid('tëst')).toBe(false); + expect(isSubdomainValid('tést')).toBe(false); + expect(isSubdomainValid('tèst')).toBe(false); + expect(isSubdomainValid('café')).toBe(false); + expect(isSubdomainValid('naïve')).toBe(false); + }); + + it('should handle numeric strings', () => { + expect(isSubdomainValid('123')).toBe(true); + expect(isSubdomainValid('456789')).toBe(true); + expect(isSubdomainValid('1-2-3')).toBe(true); + }); + }); + + describe('pattern validation specifics', () => { + it('should enforce the exact regex pattern requirements', () => { + // Test that the pattern requires alphanumeric start and end + expect(isSubdomainValid('a-b')).toBe(true); + expect(isSubdomainValid('1-2')).toBe(true); + expect(isSubdomainValid('test-123')).toBe(true); + + // Test that it rejects patterns not matching the regex + expect(isSubdomainValid('-ab')).toBe(false); + expect(isSubdomainValid('ab-')).toBe(false); + }); + + it('should reject api- prefix specifically', () => { + expect(isSubdomainValid('api-anything')).toBe(false); + expect(isSubdomainValid('api-test')).toBe(false); + expect(isSubdomainValid('api-123')).toBe(false); + + // But allow 'api' in other positions + expect(isSubdomainValid('myapi')).toBe(true); + }); + + it('should validate length constraints from regex', () => { + // The regex pattern is: /^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/ + // This means: start char + 1-28 middle chars + end char = 3-30 total chars + + // 3 chars: start + 1 middle + end + expect(isSubdomainValid('abc')).toBe(true); + + // 30 chars: start + 28 middle + end + const thirtyChars = 'a' + 'b'.repeat(28) + 'c'; + + expect(thirtyChars.length).toBe(30); + expect(isSubdomainValid(thirtyChars)).toBe(true); + }); + }); +}); diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/generate-random-subdomain.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/generate-random-subdomain.util.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/domain-manager/utils/generate-random-subdomain.ts rename to packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/generate-random-subdomain.util.ts diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/get-subdomain-from-email.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util.ts similarity index 88% rename from packages/twenty-server/src/engine/core-modules/domain-manager/utils/get-subdomain-from-email.ts rename to packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util.ts index 194a48d8e1..a9c2e58600 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/get-subdomain-from-email.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-from-email.util.ts @@ -8,5 +8,5 @@ export const getSubdomainFromEmail = (email?: string) => { const domain = getDomainNameByEmail(email); - return domain.split('.')[0]; + return domain.split('.')[0].toLowerCase(); }; diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util.ts similarity index 100% rename from packages/twenty-server/src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name.ts rename to packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/get-subdomain-name-from-display-name.util.ts diff --git a/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util.ts b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util.ts new file mode 100644 index 0000000000..0fa30d86d7 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/subdomain-manager/utils/is-subdomain-valid.util.ts @@ -0,0 +1,9 @@ +import { RESERVED_SUBDOMAINS } from 'src/engine/core-modules/workspace/constants/reserved-subdomains.constant'; +import { VALID_SUBDOMAIN_PATTERN } from 'src/engine/core-modules/workspace/constants/valid-subdomain-pattern.constant'; + +export const isSubdomainValid = (subdomain: string) => { + return ( + VALID_SUBDOMAIN_PATTERN.test(subdomain) && + !RESERVED_SUBDOMAINS.includes(subdomain.toLowerCase()) + ); +}; diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/__test__/workspace-domains.service.spec.ts similarity index 81% rename from packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts rename to packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/__test__/workspace-domains.service.spec.ts index c1da8dc7fd..d0d8394cba 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/__test__/workspace-domains.service.spec.ts @@ -3,64 +3,14 @@ import { getRepositoryToken } from '@nestjs/typeorm'; import { type Repository } from 'typeorm'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; -import { DomainManagerService } from './domain-manager.service'; - -describe('DomainManagerService', () => { - describe('getWorkspaceUrls', () => { - it('should return a URL containing the correct customDomain if customDomain is provided', () => { - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - const env = { - FRONTEND_URL: 'https://example.com', - }; - - // @ts-expect-error legacy noImplicitAny - return env[key]; - }); - - const result = domainManagerService.getWorkspaceUrls({ - subdomain: 'subdomain', - customDomain: 'custom-host.com', - isCustomDomainEnabled: true, - }); - - expect(result).toEqual({ - customUrl: 'https://custom-host.com/', - subdomainUrl: 'https://example.com/', - }); - }); - - it('should return a URL containing the correct subdomain if customDomain is not provided but subdomain is', () => { - jest - .spyOn(twentyConfigService, 'get') - .mockImplementation((key: string) => { - const env = { - FRONTEND_URL: 'https://example.com', - IS_MULTIWORKSPACE_ENABLED: true, - }; - - // @ts-expect-error legacy noImplicitAny - return env[key]; - }); - - const result = domainManagerService.getWorkspaceUrls({ - subdomain: 'subdomain', - customDomain: null, - isCustomDomainEnabled: false, - }); - - expect(result).toEqual({ - customUrl: undefined, - subdomainUrl: 'https://subdomain.example.com/', - }); - }); - }); - let domainManagerService: DomainManagerService; +describe('WorkspaceDomainsService', () => { + let workspaceDomainsService: WorkspaceDomainsService; let twentyConfigService: TwentyConfigService; let workspaceRepository: Repository; let publicDomainRepository: Repository; @@ -68,7 +18,8 @@ describe('DomainManagerService', () => { beforeEach(async () => { const module: TestingModule = await Test.createTestingModule({ providers: [ - DomainManagerService, + DomainServerConfigService, + WorkspaceDomainsService, { provide: getRepositoryToken(Workspace), useValue: { @@ -97,13 +48,15 @@ describe('DomainManagerService', () => { publicDomainRepository = module.get>( getRepositoryToken(PublicDomain), ); - domainManagerService = - module.get(DomainManagerService); + workspaceDomainsService = module.get( + WorkspaceDomainsService, + ); + twentyConfigService = module.get(TwentyConfigService); }); - describe('buildBaseUrl', () => { - it('should build the base URL from environment variables', () => { + describe('getWorkspaceUrls', () => { + it('should return a URL containing the correct customDomain if customDomain is provided', () => { jest .spyOn(twentyConfigService, 'get') .mockImplementation((key: string) => { @@ -115,28 +68,41 @@ describe('DomainManagerService', () => { return env[key]; }); - const result = domainManagerService.getBaseUrl(); + const result = workspaceDomainsService.getWorkspaceUrls({ + subdomain: 'subdomain', + customDomain: 'custom-host.com', + isCustomDomainEnabled: true, + }); - expect(result.toString()).toBe('https://example.com/'); + expect(result).toEqual({ + customUrl: 'https://custom-host.com/', + subdomainUrl: 'https://example.com/', + }); }); - it('should append default subdomain if multiworkspace is enabled', () => { + it('should return a URL containing the correct subdomain if customDomain is not provided but subdomain is', () => { jest .spyOn(twentyConfigService, 'get') .mockImplementation((key: string) => { const env = { FRONTEND_URL: 'https://example.com', IS_MULTIWORKSPACE_ENABLED: true, - DEFAULT_SUBDOMAIN: 'test', }; // @ts-expect-error legacy noImplicitAny return env[key]; }); - const result = domainManagerService.getBaseUrl(); + const result = workspaceDomainsService.getWorkspaceUrls({ + subdomain: 'subdomain', + customDomain: null, + isCustomDomainEnabled: false, + }); - expect(result.toString()).toBe('https://test.example.com/'); + expect(result).toEqual({ + customUrl: undefined, + subdomainUrl: 'https://subdomain.example.com/', + }); }); }); @@ -155,7 +121,7 @@ describe('DomainManagerService', () => { return env[key]; }); - const result = domainManagerService.buildWorkspaceURL({ + const result = workspaceDomainsService.buildWorkspaceURL({ workspace: { subdomain: 'test', customDomain: null, @@ -178,7 +144,7 @@ describe('DomainManagerService', () => { return env[key]; }); - const result = domainManagerService.buildWorkspaceURL({ + const result = workspaceDomainsService.buildWorkspaceURL({ workspace: { subdomain: 'test', customDomain: null, @@ -202,7 +168,7 @@ describe('DomainManagerService', () => { return env[key]; }); - const result = domainManagerService.buildWorkspaceURL({ + const result = workspaceDomainsService.buildWorkspaceURL({ workspace: { subdomain: 'test', customDomain: null, @@ -240,7 +206,7 @@ describe('DomainManagerService', () => { ] as unknown as Workspace[]); const result = - await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( 'https://example.com', ); @@ -270,7 +236,7 @@ describe('DomainManagerService', () => { ] as unknown as Workspace[]); const result = - await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( 'https://example.com', ); @@ -296,7 +262,7 @@ describe('DomainManagerService', () => { } as unknown as Promise); const result = - await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( 'https://123.example.com', ); @@ -322,7 +288,7 @@ describe('DomainManagerService', () => { } as unknown as Promise); const result = - await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( 'https://123.custom.com', ); @@ -352,7 +318,7 @@ describe('DomainManagerService', () => { } as unknown as Promise); const result = - await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( 'https://123.custom.com', ); @@ -377,7 +343,7 @@ describe('DomainManagerService', () => { jest.spyOn(publicDomainRepository, 'findOne').mockResolvedValueOnce(null); const result = - await domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( 'https://123.custom.com', ); diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts similarity index 52% rename from packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts rename to packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts index 0c0579029b..83403970a5 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/services/domain-manager.service.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service.ts @@ -4,127 +4,48 @@ import { InjectRepository } from '@nestjs/typeorm'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { Repository } from 'typeorm'; -import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type'; -import { generateRandomSubdomain } from 'src/engine/core-modules/domain-manager/utils/generate-random-subdomain'; -import { getSubdomainFromEmail } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-from-email'; -import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { buildUrlWithPathnameAndSearchParams } from 'src/engine/core-modules/domain/domain-server-config/utils/build-url-with-pathname-and-search-params.util'; +import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; import { WorkspaceNotFoundDefaultError } from 'src/engine/core-modules/workspace/workspace.exception'; @Injectable() -export class DomainManagerService { +export class WorkspaceDomainsService { constructor( + private readonly domainServerConfigService: DomainServerConfigService, + private readonly twentyConfigService: TwentyConfigService, @InjectRepository(Workspace) private readonly workspaceRepository: Repository, @InjectRepository(PublicDomain) private readonly publicDomainRepository: Repository, - private readonly twentyConfigService: TwentyConfigService, ) {} - getFrontUrl() { - return new URL( - this.twentyConfigService.get('FRONTEND_URL') ?? - this.twentyConfigService.get('SERVER_URL'), - ); - } - - getBaseUrl(): URL { - const baseUrl = this.getFrontUrl(); - - if ( - this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED') && - this.twentyConfigService.get('DEFAULT_SUBDOMAIN') - ) { - baseUrl.hostname = `${this.twentyConfigService.get('DEFAULT_SUBDOMAIN')}.${baseUrl.hostname}`; - } - - return baseUrl; - } - - getPublicDomainUrl(): URL { - return new URL(this.twentyConfigService.get('PUBLIC_DOMAIN_URL')); - } - - private appendSearchParams( - url: URL, - searchParams: Record, - ) { - Object.entries(searchParams).forEach(([key, value]) => { - url.searchParams.set(key, value.toString()); - }); - } - - buildBaseUrl({ - pathname, - searchParams, - }: { - pathname?: string; - searchParams?: Record; - }) { - const url = this.getBaseUrl(); - - if (pathname) { - url.pathname = pathname; - } - - if (searchParams) { - this.appendSearchParams(url, searchParams); - } - - return url; - } - buildWorkspaceURL({ workspace, pathname, searchParams, }: { - workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType; + workspace: WorkspaceDomainConfig; pathname?: string; searchParams?: Record; }) { const workspaceUrls = this.getWorkspaceUrls(workspace); - const url = new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl); - - if (pathname) { - url.pathname = pathname; - } - - if (searchParams) { - this.appendSearchParams(url, searchParams); - } + const url = buildUrlWithPathnameAndSearchParams({ + baseUrl: new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl), + pathname, + searchParams, + }); return url; } - getSubdomainAndDomainFromUrl = (url: string) => { - const { hostname: originHostname } = new URL(url); - - const frontDomain = this.getFrontUrl().hostname; - - const isFrontdomain = originHostname.endsWith(`.${frontDomain}`); - - const subdomain = originHostname.replace(`.${frontDomain}`, ''); - - return { - subdomain: - isFrontdomain && !this.isDefaultSubdomain(subdomain) - ? subdomain - : undefined, - domain: isFrontdomain ? null : originHostname, - }; - }; - - isDefaultSubdomain(subdomain: string) { - return subdomain === this.twentyConfigService.get('DEFAULT_SUBDOMAIN'); - } - - computeRedirectErrorUrl( + computeWorkspaceRedirectErrorUrl( errorMessage: string, - workspace: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType, + workspace: WorkspaceDomainConfig, pathname: string, ) { const url = this.buildWorkspaceURL({ @@ -168,7 +89,8 @@ export class DomainManagerService { return this.getDefaultWorkspace(); } - const { subdomain, domain } = this.getSubdomainAndDomainFromUrl(origin); + const { subdomain, domain } = + this.domainServerConfigService.getSubdomainAndDomainFromUrl(origin); if (!domain && !subdomain) return; @@ -195,29 +117,8 @@ export class DomainManagerService { return publicDomainFromCustomDomain?.workspace; } - private extractSubdomain(params?: { email?: string; displayName?: string }) { - if (params?.email) { - return getSubdomainFromEmail(params.email); - } - - if (params?.displayName) { - return getSubdomainNameFromDisplayName(params.displayName); - } - } - - async generateSubdomain(params?: { email?: string; displayName?: string }) { - const subdomain = - this.extractSubdomain(params) ?? generateRandomSubdomain(); - - const existingWorkspaceCount = await this.workspaceRepository.countBy({ - subdomain, - }); - - return `${subdomain}${existingWorkspaceCount > 0 ? `-${Math.random().toString(36).substring(2, 10)}` : ''}`; - } - private getCustomWorkspaceUrl(customDomain: string) { - const url = this.getFrontUrl(); + const url = this.domainServerConfigService.getFrontUrl(); url.hostname = customDomain; @@ -225,7 +126,7 @@ export class DomainManagerService { } private getTwentyWorkspaceUrl(subdomain: string) { - const url = this.getFrontUrl(); + const url = this.domainServerConfigService.getFrontUrl(); url.hostname = this.twentyConfigService.get('IS_MULTIWORKSPACE_ENABLED') ? `${subdomain}.${url.hostname}` @@ -235,7 +136,7 @@ export class DomainManagerService { } getSubdomainAndCustomDomainFromWorkspaceFallbackOnDefaultSubdomain( - workspace?: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType | null, + workspace?: WorkspaceDomainConfig | null, ) { if (!workspace) { return { @@ -258,7 +159,7 @@ export class DomainManagerService { subdomain, customDomain, isCustomDomainEnabled, - }: WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType) { + }: WorkspaceDomainConfig) { return { customUrl: isCustomDomainEnabled && customDomain @@ -267,4 +168,8 @@ export class DomainManagerService { subdomainUrl: this.getTwentyWorkspaceUrl(subdomain), }; } + + async findByCustomDomain(customDomain: string) { + return this.workspaceRepository.findOne({ where: { customDomain } }); + } } diff --git a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.type.ts b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type.ts similarity index 66% rename from packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.type.ts rename to packages/twenty-server/src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type.ts index 5ad3777f5b..4c8073039a 100644 --- a/packages/twenty-server/src/engine/core-modules/domain-manager/domain-manager.type.ts +++ b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type.ts @@ -1,6 +1,6 @@ import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -export type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType = Pick< +export type WorkspaceDomainConfig = Pick< Workspace, 'subdomain' | 'customDomain' | 'isCustomDomainEnabled' >; diff --git a/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/workspace-domains.module.ts b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/workspace-domains.module.ts new file mode 100644 index 0000000000..5f7e21f522 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/domain/workspace-domains/workspace-domains.module.ts @@ -0,0 +1,17 @@ +import { Module } from '@nestjs/common'; +import { TypeOrmModule } from '@nestjs/typeorm'; + +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; +import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; + +@Module({ + imports: [ + DomainServerConfigModule, + TypeOrmModule.forFeature([Workspace, PublicDomain]), + ], + providers: [WorkspaceDomainsService], + exports: [WorkspaceDomainsService], +}) +export class WorkspaceDomainsModule {} diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts index fb9d3a3516..5052e6906c 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.module.ts @@ -3,23 +3,25 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { EmailVerificationResolver } from 'src/engine/core-modules/email-verification/email-verification.resolver'; import { EmailVerificationService } from 'src/engine/core-modules/email-verification/services/email-verification.service'; import { EmailModule } from 'src/engine/core-modules/email/email.module'; import { TwentyConfigModule } from 'src/engine/core-modules/twenty-config/twenty-config.module'; import { UserWorkspaceModule } from 'src/engine/core-modules/user-workspace/user-workspace.module'; -import { UserModule } from 'src/engine/core-modules/user/user.module'; import { User } from 'src/engine/core-modules/user/user.entity'; +import { UserModule } from 'src/engine/core-modules/user/user.module'; @Module({ imports: [ TypeOrmModule.forFeature([AppToken, User]), EmailModule, TwentyConfigModule, - DomainManagerModule, UserModule, UserWorkspaceModule, + WorkspaceDomainsModule, + DomainServerConfigModule, ], providers: [ EmailVerificationService, diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts index f5443a8a4e..ea00f0b31f 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/email-verification.resolver.ts @@ -1,7 +1,7 @@ import { UseFilters, UseGuards, UsePipes } from '@nestjs/common'; import { Args, Context, Mutation, Resolver } from '@nestjs/graphql'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { ResendEmailVerificationTokenInput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.input'; import { ResendEmailVerificationTokenOutput } from 'src/engine/core-modules/email-verification/dtos/resend-email-verification-token.output'; import { EmailVerificationExceptionFilter } from 'src/engine/core-modules/email-verification/email-verification-exception-filter.util'; @@ -20,7 +20,7 @@ import { PublicEndpointGuard } from 'src/engine/guards/public-endpoint.guard'; export class EmailVerificationResolver { constructor( private readonly emailVerificationService: EmailVerificationService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} // TODO: this should be an authenticated endpoint @@ -33,7 +33,7 @@ export class EmailVerificationResolver { @Context() context: I18nContext, ): Promise { const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, ); diff --git a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts index 11dfff1306..d0f5491a28 100644 --- a/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts +++ b/packages/twenty-server/src/engine/core-modules/email-verification/services/email-verification.service.ts @@ -16,8 +16,9 @@ import { AppTokenType, } from 'src/engine/core-modules/app-token/app-token.entity'; import { EmailVerificationTokenService } from 'src/engine/core-modules/auth/token/services/email-verification-token.service'; -import { type WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType } from 'src/engine/core-modules/domain-manager/domain-manager.type'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; +import { WorkspaceDomainConfig } from 'src/engine/core-modules/domain/workspace-domains/types/workspace-domain-config.type'; import { EmailVerificationException, EmailVerificationExceptionCode, @@ -32,7 +33,8 @@ export class EmailVerificationService { constructor( @InjectRepository(AppToken) private readonly appTokenRepository: Repository, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, + private readonly domainsServerConfigService: DomainServerConfigService, private readonly emailService: EmailService, private readonly twentyConfigService: TwentyConfigService, private readonly userService: UserService, @@ -43,9 +45,7 @@ export class EmailVerificationService { async sendVerificationEmail( userId: string, email: string, - workspace: - | WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType - | undefined, + workspace: WorkspaceDomainConfig | undefined, locale: keyof typeof APP_LOCALES, verifyEmailRedirectPath?: string, ) { @@ -67,11 +67,13 @@ export class EmailVerificationService { }, }; const verificationLink = workspace - ? this.domainManagerService.buildWorkspaceURL({ + ? this.workspaceDomainsService.buildWorkspaceURL({ workspace, ...linkPathnameAndSearchParams, }) - : this.domainManagerService.buildBaseUrl(linkPathnameAndSearchParams); + : this.domainsServerConfigService.buildBaseUrl( + linkPathnameAndSearchParams, + ); const emailData = { link: verificationLink.toString(), @@ -104,9 +106,7 @@ export class EmailVerificationService { async resendEmailVerificationToken( email: string, - workspace: - | WorkspaceSubdomainCustomDomainAndIsCustomDomainEnabledType - | undefined, + workspace: WorkspaceDomainConfig | undefined, locale: keyof typeof APP_LOCALES, ) { if (!this.twentyConfigService.get('IS_EMAIL_VERIFICATION_REQUIRED')) { diff --git a/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts b/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts index 30773322bb..9a18b5f629 100644 --- a/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts +++ b/packages/twenty-server/src/engine/core-modules/guard-redirect/guard-redirect.module.ts @@ -1,10 +1,11 @@ import { Module } from '@nestjs/common'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { DomainServerConfigModule } from 'src/engine/core-modules/domain/domain-server-config/domain-server-config.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { GuardRedirectService } from 'src/engine/core-modules/guard-redirect/services/guard-redirect.service'; @Module({ - imports: [DomainManagerModule], + imports: [WorkspaceDomainsModule, DomainServerConfigModule], providers: [GuardRedirectService], exports: [GuardRedirectService], }) diff --git a/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts b/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts index 9bd0c00d06..50c9d510c2 100644 --- a/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts +++ b/packages/twenty-server/src/engine/core-modules/guard-redirect/services/guard-redirect.service.ts @@ -1,4 +1,4 @@ -import { type ExecutionContext, Injectable } from '@nestjs/common'; +import { ExecutionContext, Injectable } from '@nestjs/common'; import { type Request } from 'express'; import { AppPath } from 'twenty-shared/types'; @@ -7,7 +7,8 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { DomainServerConfigService } from 'src/engine/core-modules/domain/domain-server-config/services/domain-server-config.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { type CustomException } from 'src/utils/custom-exception'; @@ -15,9 +16,10 @@ import { type CustomException } from 'src/utils/custom-exception'; @Injectable() export class GuardRedirectService { constructor( - private readonly domainManagerService: DomainManagerService, private readonly twentyConfigService: TwentyConfigService, private readonly exceptionHandlerService: ExceptionHandlerService, + private readonly domainsServerConfigService: DomainServerConfigService, + private readonly workspaceDomainsService: WorkspaceDomainsService, ) {} dispatchErrorFromGuard( @@ -48,7 +50,7 @@ export class GuardRedirectService { const request = context.switchToHttp().getRequest(); const subdomainAndDomainFromReferer = request.headers.referer - ? this.domainManagerService.getSubdomainAndDomainFromUrl( + ? this.domainsServerConfigService.getSubdomainAndDomainFromUrl( request.headers.referer, ) : null; @@ -95,7 +97,7 @@ export class GuardRedirectService { }) { this.captureException(error, workspace.id); - return this.domainManagerService.computeRedirectErrorUrl( + return this.workspaceDomainsService.computeWorkspaceRedirectErrorUrl( error instanceof AuthException ? error.message : 'Unknown error', { subdomain: workspace.subdomain, diff --git a/packages/twenty-server/src/engine/core-modules/impersonation/__tests__/impersonation.service.spec.ts b/packages/twenty-server/src/engine/core-modules/impersonation/__tests__/impersonation.service.spec.ts index e31e7edcd1..74f11f8444 100644 --- a/packages/twenty-server/src/engine/core-modules/impersonation/__tests__/impersonation.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/impersonation/__tests__/impersonation.service.spec.ts @@ -9,7 +9,7 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { ImpersonationService } from 'src/engine/core-modules/impersonation/services/impersonation.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { OTPStatus } from 'src/engine/core-modules/two-factor-authentication/strategies/otp/otp.constants'; @@ -61,7 +61,7 @@ describe('ImpersonationService', () => { }, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { getWorkspaceUrls: jest.fn().mockReturnValue({ customUrl: undefined, diff --git a/packages/twenty-server/src/engine/core-modules/impersonation/impersonation.module.ts b/packages/twenty-server/src/engine/core-modules/impersonation/impersonation.module.ts index 6534d0aefc..a194d8f8d6 100644 --- a/packages/twenty-server/src/engine/core-modules/impersonation/impersonation.module.ts +++ b/packages/twenty-server/src/engine/core-modules/impersonation/impersonation.module.ts @@ -3,7 +3,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { ImpersonationResolver } from 'src/engine/core-modules/impersonation/impersonation.resolver'; import { ImpersonationService } from 'src/engine/core-modules/impersonation/services/impersonation.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -23,7 +23,7 @@ import { UserRoleModule } from 'src/engine/metadata-modules/user-role/user-role. UserRoleModule, AuditModule, TypeOrmModule.forFeature([UserWorkspace, Workspace, User]), - DomainManagerModule, + WorkspaceDomainsModule, PermissionsModule, ], providers: [ImpersonationService, ImpersonationResolver], diff --git a/packages/twenty-server/src/engine/core-modules/impersonation/services/impersonation.service.ts b/packages/twenty-server/src/engine/core-modules/impersonation/services/impersonation.service.ts index fef3f784aa..0d7232bb5f 100644 --- a/packages/twenty-server/src/engine/core-modules/impersonation/services/impersonation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/impersonation/services/impersonation.service.ts @@ -13,7 +13,7 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { twoFactorAuthenticationMethodsValidator } from 'src/engine/core-modules/two-factor-authentication/two-factor-authentication.validation'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -25,7 +25,7 @@ import { PermissionsService } from 'src/engine/metadata-modules/permissions/perm export class ImpersonationService { constructor( private readonly auditService: AuditService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly loginTokenService: LoginTokenService, private readonly twentyConfigService: TwentyConfigService, @InjectRepository(UserWorkspace) @@ -172,7 +172,7 @@ export class ImpersonationService { return { workspace: { id: toImpersonateUserWorkspace.workspace.id, - workspaceUrls: this.domainManagerService.getWorkspaceUrls( + workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls( toImpersonateUserWorkspace.workspace, ), }, diff --git a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts index c8a9123589..fde38f7b11 100644 --- a/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts +++ b/packages/twenty-server/src/engine/core-modules/sso/sso.module.ts @@ -6,7 +6,6 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; import { FeatureFlag } from 'src/engine/core-modules/feature-flag/feature-flag.entity'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { GuardRedirectModule } from 'src/engine/core-modules/guard-redirect/guard-redirect.module'; @@ -24,7 +23,6 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi FeatureFlag, ]), BillingModule, - DomainManagerModule, GuardRedirectModule, PermissionsModule, FeatureFlagModule, diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.module.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.module.ts index 6eb47d7dfe..1f88abc998 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.module.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { JwtModule } from 'src/engine/core-modules/jwt/jwt.module'; import { MetricsModule } from 'src/engine/core-modules/metrics/metrics.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -13,13 +13,13 @@ import { UserModule } from 'src/engine/core-modules/user/user.module'; import { TwoFactorAuthenticationResolver } from './two-factor-authentication.resolver'; import { TwoFactorAuthenticationService } from './two-factor-authentication.service'; -import { SimpleSecretEncryptionUtil } from './utils/simple-secret-encryption.util'; import { TwoFactorAuthenticationMethod } from './entities/two-factor-authentication-method.entity'; +import { SimpleSecretEncryptionUtil } from './utils/simple-secret-encryption.util'; @Module({ imports: [ UserWorkspaceModule, - DomainManagerModule, + WorkspaceDomainsModule, MetricsModule, TokenModule, JwtModule, diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts index 8a18a09743..85b2a20dc5 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.spec.ts @@ -6,7 +6,7 @@ import { AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { type User } from 'src/engine/core-modules/user/user.entity'; import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -37,7 +37,7 @@ const createMockUserService = () => ({ findUserByEmailOrThrow: jest.fn(), }); -const createMockDomainManagerService = () => ({ +const createMockWorkspaceDomainsService = () => ({ getWorkspaceByOriginOrDefaultWorkspace: jest.fn(), }); @@ -48,7 +48,9 @@ describe('TwoFactorAuthenticationResolver', () => { >; let loginTokenService: ReturnType; let userService: ReturnType; - let domainManagerService: ReturnType; + let workspaceDomainsService: ReturnType< + typeof createMockWorkspaceDomainsService + >; let repository: ReturnType; const mockUser: User = { @@ -86,8 +88,8 @@ describe('TwoFactorAuthenticationResolver', () => { useFactory: createMockUserService, }, { - provide: DomainManagerService, - useFactory: createMockDomainManagerService, + provide: WorkspaceDomainsService, + useFactory: createMockWorkspaceDomainsService, }, { provide: getRepositoryToken(TwoFactorAuthenticationMethod), @@ -102,7 +104,7 @@ describe('TwoFactorAuthenticationResolver', () => { twoFactorAuthenticationService = module.get(TwoFactorAuthenticationService); loginTokenService = module.get(LoginTokenService); userService = module.get(UserService); - domainManagerService = module.get(DomainManagerService); + workspaceDomainsService = module.get(WorkspaceDomainsService); repository = module.get(getRepositoryToken(TwoFactorAuthenticationMethod)); }); @@ -125,7 +127,7 @@ describe('TwoFactorAuthenticationResolver', () => { sub: mockUser.email, workspaceId: mockWorkspace.id, }); - domainManagerService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue( + workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue( mockWorkspace, ); userService.findUserByEmailOrThrow.mockResolvedValue(mockUser); @@ -144,7 +146,7 @@ describe('TwoFactorAuthenticationResolver', () => { mockInput.loginToken, ); expect( - domainManagerService.getWorkspaceByOriginOrDefaultWorkspace, + workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace, ).toHaveBeenCalledWith(origin); expect(userService.findUserByEmailOrThrow).toHaveBeenCalledWith( mockUser.email, @@ -160,7 +162,7 @@ describe('TwoFactorAuthenticationResolver', () => { }); it('should throw WORKSPACE_NOT_FOUND when workspace is not found', async () => { - domainManagerService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue( + workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace.mockResolvedValue( null, ); diff --git a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.ts b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.ts index ab8f41b0cd..c0485c3fbe 100644 --- a/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.ts +++ b/packages/twenty-server/src/engine/core-modules/two-factor-authentication/two-factor-authentication.resolver.ts @@ -11,7 +11,7 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { AuthGraphqlApiExceptionFilter } from 'src/engine/core-modules/auth/filters/auth-graphql-api-exception.filter'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { UserService } from 'src/engine/core-modules/user/services/user.service'; import { User } from 'src/engine/core-modules/user/user.entity'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; @@ -39,7 +39,7 @@ export class TwoFactorAuthenticationResolver { private readonly twoFactorAuthenticationService: TwoFactorAuthenticationService, private readonly loginTokenService: LoginTokenService, private readonly userService: UserService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, @InjectRepository(TwoFactorAuthenticationMethod) private readonly twoFactorAuthenticationMethodRepository: Repository, ) {} @@ -57,7 +57,7 @@ export class TwoFactorAuthenticationResolver { ); const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, ); diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts index ac4cc3aea5..06c03a498c 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.module.ts @@ -6,7 +6,7 @@ import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { TypeORMModule } from 'src/database/typeorm/typeorm.module'; import { ApprovedAccessDomainModule } from 'src/engine/core-modules/approved-access-domain/approved-access-domain.module'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -31,7 +31,7 @@ import { WorkspaceDataSourceModule } from 'src/engine/workspace-datasource/works WorkspaceDataSourceModule, ApprovedAccessDomainModule, WorkspaceInvitationModule, - DomainManagerModule, + WorkspaceDomainsModule, TwentyORMModule, UserRoleModule, FileUploadModule, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts index 8b779d4838..1d69a6e679 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.spec.ts @@ -9,7 +9,7 @@ import { type ApprovedAccessDomain } from 'src/engine/core-modules/approved-acce import { ApprovedAccessDomainService } from 'src/engine/core-modules/approved-access-domain/services/approved-access-domain.service'; import { AuthException } from 'src/engine/core-modules/auth/auth.exception'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FileStorageService } from 'src/engine/core-modules/file-storage/file-storage.service'; import { FileUploadService, @@ -81,7 +81,7 @@ describe('UserWorkspaceService', () => { }, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { getWorkspaceUrls: jest.fn(), }, diff --git a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts index f81c189d44..5122fb6d6d 100644 --- a/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/user-workspace/user-workspace.service.ts @@ -16,7 +16,7 @@ import { } from 'src/engine/core-modules/auth/auth.exception'; import { type AvailableWorkspace } from 'src/engine/core-modules/auth/dto/available-workspaces.output'; import { LoginTokenService } from 'src/engine/core-modules/auth/token/services/login-token.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { FileUploadService } from 'src/engine/core-modules/file/file-upload/services/file-upload.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; @@ -43,7 +43,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { @InjectRepository(User) private readonly userRepository: Repository, private readonly workspaceInvitationService: WorkspaceInvitationService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly loginTokenService: LoginTokenService, private readonly approvedAccessDomainService: ApprovedAccessDomainService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, @@ -384,7 +384,7 @@ export class UserWorkspaceService extends TypeOrmQueryService { return { id: workspace.id, displayName: workspace.displayName, - workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace), + workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace), logo: workspace.logo ? this.fileService.signFileUrl({ url: workspace.logo, diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts index 4210b9343f..3a0e431c54 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.spec.ts @@ -7,7 +7,7 @@ import { AppToken, AppTokenType, } from 'src/engine/core-modules/app-token/app-token.entity'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -60,7 +60,7 @@ describe('WorkspaceInvitationService', () => { useClass: Repository, }, { - provide: DomainManagerService, + provide: WorkspaceDomainsService, useValue: { buildWorkspaceURL: jest .fn() diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts index 3ae94aa238..fe80abdd40 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/services/workspace-invitation.service.ts @@ -20,7 +20,7 @@ import { AuthException, AuthExceptionCode, } from 'src/engine/core-modules/auth/auth.exception'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { FileService } from 'src/engine/core-modules/file/services/file.service'; import { I18nService } from 'src/engine/core-modules/i18n/i18n.service'; @@ -46,7 +46,7 @@ export class WorkspaceInvitationService { private readonly twentyConfigService: TwentyConfigService, private readonly emailService: EmailService, private readonly onboardingService: OnboardingService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly i18nService: I18nService, private readonly fileService: FileService, ) {} @@ -281,7 +281,7 @@ export class WorkspaceInvitationService { for (const invitation of invitationsPr) { if (invitation.status === 'fulfilled') { - const link = this.domainManagerService.buildWorkspaceURL({ + const link = this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname: getAppPath(AppPath.Invite, { workspaceInviteHash: workspace?.inviteHash, diff --git a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.module.ts b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.module.ts index 214c324cb7..a0a34beb62 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace-invitation/workspace-invitation.module.ts @@ -3,7 +3,7 @@ import { Module } from '@nestjs/common'; import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm'; import { AppToken } from 'src/engine/core-modules/app-token/app-token.entity'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; import { OnboardingModule } from 'src/engine/core-modules/onboarding/onboarding.module'; @@ -15,7 +15,7 @@ import { PermissionsModule } from 'src/engine/metadata-modules/permissions/permi @Module({ imports: [ - DomainManagerModule, + WorkspaceDomainsModule, NestjsQueryTypeOrmModule.forFeature([AppToken, UserWorkspace, Workspace]), FileModule, OnboardingModule, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/constants/reserved-subdomains.constant.ts b/packages/twenty-server/src/engine/core-modules/workspace/constants/reserved-subdomains.constant.ts new file mode 100644 index 0000000000..00aed51267 --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workspace/constants/reserved-subdomains.constant.ts @@ -0,0 +1,122 @@ +export const RESERVED_SUBDOMAINS = [ + 'trust', + 'demo', + 'api', + 't', + 'companies', + 'telemetry', + 'logs', + 'metrics', + 'next', + 'main', + 'admin', + 'dashboard', + 'dash', + 'billing', + 'db', + 'favicon', + 'www', + 'mail', + 'docs', + 'dev', + 'app', + 'staging', + 'production', + 'developer', + 'files', + 'cdn', + 'storage', + 'about', + 'help', + 'support', + 'contact', + 'privacy', + 'terms', + 'careers', + 'jobs', + 'blog', + 'news', + 'events', + 'community', + 'forum', + 'chat', + 'test', + 'testing', + 'feedback', + 'config', + 'settings', + 'media', + 'image', + 'audio', + 'video', + 'images', + 'partners', + 'partnership', + 'partnerships', + 'assets', + 'login', + 'signin', + 'signup', + 'legal', + 'shop', + 'merch', + 'store', + 'auth', + 'register', + 'payment', + 'fr', + 'de', + 'it', + 'es', + 'pt', + 'nl', + 'be', + 'ch', + 'us', + 'ca', + 'au', + 'nz', + 'za', + 'eu', + 'uk', + 'ru', + 'ua', + 'pl', + 'ro', + 'bg', + 'gr', + 'cz', + 'sk', + 'hu', + 'hr', + 'si', + 'rs', + 'me', + 'ba', + 'mk', + 'al', + 'az', + 'tr', + 'cy', + 'lv', + 'lt', + 'ee', + 'fi', + 'is', + 'no', + 'se', + 'dk', + 'asia', + 'africa', + 'america', + 'europe', + 'north-america', + 'south-africa', + 'north-africa', + 'south-america', + 'oceania', + 'paris', + 'london', + 'new-york', + 'san-francisco', +]; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/constants/valid-subdomain-pattern.constant.ts b/packages/twenty-server/src/engine/core-modules/workspace/constants/valid-subdomain-pattern.constant.ts new file mode 100644 index 0000000000..3678e1e07b --- /dev/null +++ b/packages/twenty-server/src/engine/core-modules/workspace/constants/valid-subdomain-pattern.constant.ts @@ -0,0 +1,2 @@ +export const VALID_SUBDOMAIN_PATTERN = + /^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/; diff --git a/packages/twenty-server/src/engine/core-modules/workspace/crons/jobs/check-custom-domain-valid-records.cron.job.ts b/packages/twenty-server/src/engine/core-modules/workspace/crons/jobs/check-custom-domain-valid-records.cron.job.ts index 28a3cff13b..6658fe0182 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/crons/jobs/check-custom-domain-valid-records.cron.job.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/crons/jobs/check-custom-domain-valid-records.cron.job.ts @@ -1,14 +1,14 @@ import { InjectRepository } from '@nestjs/typeorm'; -import { IsNull, Not, Repository, Raw } from 'typeorm'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; +import { IsNull, Not, Raw, Repository } from 'typeorm'; +import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; +import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service'; import { Process } from 'src/engine/core-modules/message-queue/decorators/process.decorator'; import { Processor } from 'src/engine/core-modules/message-queue/decorators/processor.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; -import { SentryCronMonitor } from 'src/engine/core-modules/cron/sentry-cron-monitor.decorator'; -import { WorkspaceService } from 'src/engine/core-modules/workspace/services/workspace.service'; export const CHECK_CUSTOM_DOMAIN_VALID_RECORDS_CRON_PATTERN = '0 * * * *'; @@ -17,7 +17,7 @@ export class CheckCustomDomainValidRecordsCronJob { constructor( @InjectRepository(Workspace) private readonly workspaceRepository: Repository, - private readonly workspaceService: WorkspaceService, + private readonly customDomainManagerService: CustomDomainManagerService, ) {} @Process(CheckCustomDomainValidRecordsCronJob.name) @@ -39,7 +39,9 @@ export class CheckCustomDomainValidRecordsCronJob { for (const workspace of workspaces) { try { - await this.workspaceService.checkCustomDomainValidRecords(workspace); + await this.customDomainManagerService.checkCustomDomainValidRecords( + workspace, + ); } catch (error) { throw new Error( `[${CheckCustomDomainValidRecordsCronJob.name}] Cannot check custom domain for workspaces: ${error.message}`, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts b/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts index 1df2ce49f1..9a4b1e31f8 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/dtos/update-workspace-input.ts @@ -3,7 +3,6 @@ import { Field, InputType } from '@nestjs/graphql'; import { IsBoolean, IsInt, - IsNotIn, IsOptional, IsString, IsUUID, @@ -18,129 +17,6 @@ export class UpdateWorkspaceInput { @Field({ nullable: true }) @IsString() @IsOptional() - @Matches(/^(?!api-).*^[a-z0-9][a-z0-9-]{1,28}[a-z0-9]$/) - @IsNotIn([ - 'trust', - 'demo', - 'api', - 't', - 'companies', - 'telemetry', - 'logs', - 'metrics', - 'next', - 'main', - 'admin', - 'dashboard', - 'dash', - 'billing', - 'db', - 'favicon', - 'www', - 'mail', - 'docs', - 'dev', - 'app', - 'staging', - 'production', - 'developer', - 'files', - 'cdn', - 'storage', - 'about', - 'help', - 'support', - 'contact', - 'privacy', - 'terms', - 'careers', - 'jobs', - 'blog', - 'news', - 'events', - 'community', - 'forum', - 'chat', - 'test', - 'testing', - 'feedback', - 'config', - 'settings', - 'media', - 'image', - 'audio', - 'video', - 'images', - 'partners', - 'partnership', - 'partnerships', - 'assets', - 'login', - 'signin', - 'signup', - 'legal', - 'shop', - 'merch', - 'store', - 'auth', - 'register', - 'payment', - 'fr', - 'de', - 'it', - 'es', - 'pt', - 'nl', - 'be', - 'ch', - 'us', - 'ca', - 'au', - 'nz', - 'za', - 'eu', - 'uk', - 'ru', - 'ua', - 'pl', - 'ro', - 'bg', - 'gr', - 'cz', - 'sk', - 'hu', - 'hr', - 'si', - 'rs', - 'me', - 'ba', - 'mk', - 'al', - 'az', - 'tr', - 'cy', - 'lv', - 'lt', - 'ee', - 'fi', - 'is', - 'no', - 'se', - 'dk', - 'asia', - 'africa', - 'america', - 'europe', - 'north-america', - 'south-africa', - 'north-africa', - 'south-america', - 'oceania', - 'paris', - 'london', - 'new-york', - 'san-francisco', - ]) subdomain?: string; @Field({ nullable: true }) diff --git a/packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts similarity index 97% rename from packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts rename to packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts index ce06e2d2d0..8a7f6e5726 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/__tests__/workspace.service.spec.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/__tests__/workspace.service.spec.ts @@ -7,7 +7,8 @@ import { AuditService } from 'src/engine/core-modules/audit/services/audit.servi import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { CustomDomainManagerService } from 'src/engine/core-modules/domain/custom-domain-manager/services/custom-domain-manager.service'; +import { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service'; import { EmailService } from 'src/engine/core-modules/email/email.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @@ -93,8 +94,9 @@ describe('WorkspaceService', () => { WorkspaceManagerService, UserWorkspaceService, UserService, - DomainManagerService, DnsManagerService, + CustomDomainManagerService, + SubdomainManagerService, TwentyConfigService, EmailService, OnboardingService, diff --git a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts index 2e3a311a54..ab2df529a0 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/services/workspace.service.ts @@ -9,14 +9,12 @@ import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; import { WorkspaceActivationStatus } from 'twenty-shared/workspace'; import { Repository } from 'typeorm'; -import { AuditService } from 'src/engine/core-modules/audit/services/audit.service'; -import { CUSTOM_DOMAIN_ACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-activated'; -import { CUSTOM_DOMAIN_DEACTIVATED_EVENT } from 'src/engine/core-modules/audit/utils/events/workspace-event/custom-domain/custom-domain-deactivated'; import { BillingEntitlementKey } from 'src/engine/core-modules/billing/enums/billing-entitlement-key.enum'; import { BillingSubscriptionService } from 'src/engine/core-modules/billing/services/billing-subscription.service'; import { BillingService } from 'src/engine/core-modules/billing/services/billing.service'; -import { DomainValidRecords } from 'src/engine/core-modules/dns-manager/dtos/domain-valid-records'; 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 { SubdomainManagerService } from 'src/engine/core-modules/domain/subdomain-manager/services/subdomain-manager.service'; import { ExceptionHandlerService } from 'src/engine/core-modules/exception-handler/exception-handler.service'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; import { @@ -26,7 +24,6 @@ import { import { InjectMessageQueue } from 'src/engine/core-modules/message-queue/decorators/message-queue.decorator'; import { MessageQueue } from 'src/engine/core-modules/message-queue/message-queue.constants'; import { MessageQueueService } from 'src/engine/core-modules/message-queue/services/message-queue.service'; -import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity'; import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service'; import { UserWorkspace } from 'src/engine/core-modules/user-workspace/user-workspace.entity'; import { UserWorkspaceService } from 'src/engine/core-modules/user-workspace/user-workspace.service'; @@ -60,8 +57,6 @@ export class WorkspaceService extends TypeOrmQueryService { constructor( @InjectRepository(Workspace) private readonly workspaceRepository: Repository, - @InjectRepository(PublicDomain) - private readonly publicDomainRepository: Repository, @InjectRepository(User) private readonly userRepository: Repository, @InjectRepository(UserWorkspace) @@ -77,84 +72,14 @@ export class WorkspaceService extends TypeOrmQueryService { private readonly dnsManagerService: DnsManagerService, private readonly flatEntityMapsCacheService: WorkspaceManyOrAllFlatEntityMapsCacheService, private readonly workspaceCacheStorageService: WorkspaceCacheStorageService, - private readonly auditService: AuditService, + private readonly subdomainManagerService: SubdomainManagerService, + private readonly customDomainManagerService: CustomDomainManagerService, @InjectMessageQueue(MessageQueue.deleteCascadeQueue) private readonly messageQueueService: MessageQueueService, ) { super(workspaceRepository); } - private async isCustomDomainEnabled(workspaceId: string) { - const isCustomDomainBillingEnabled = - await this.billingService.hasEntitlement( - workspaceId, - this.featureLookUpKey, - ); - - if (!isCustomDomainBillingEnabled) { - throw new WorkspaceException( - `No entitlement found for this workspace`, - WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED, - ); - } - } - - private async validateSubdomainUpdate(newSubdomain: string) { - const subdomainAvailable = await this.isSubdomainAvailable(newSubdomain); - - if ( - !subdomainAvailable || - this.twentyConfigService.get('DEFAULT_SUBDOMAIN') === newSubdomain - ) { - throw new WorkspaceException( - 'Subdomain already taken', - WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN, - ); - } - } - - private async setCustomDomain(workspace: Workspace, customDomain: string) { - await this.isCustomDomainEnabled(workspace.id); - - const existingWorkspace = await this.workspaceRepository.findOne({ - where: { customDomain }, - }); - - if (existingWorkspace && existingWorkspace.id !== workspace.id) { - throw new WorkspaceException( - 'Domain already taken', - WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN, - ); - } - - if ( - await this.publicDomainRepository.findOneBy({ - domain: customDomain, - }) - ) { - throw new WorkspaceException( - 'Domain is already registered as public domain', - WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN, - { - userFriendlyMessage: msg`Domain is already registered as public domain`, - }, - ); - } - - if (!isDefined(customDomain) || workspace.customDomain === customDomain) { - return; - } - - if (isDefined(workspace.customDomain)) { - await this.dnsManagerService.updateHostname( - workspace.customDomain, - customDomain, - ); - } else { - await this.dnsManagerService.registerHostname(customDomain); - } - } - async updateWorkspaceById({ payload, userWorkspaceId, @@ -186,7 +111,9 @@ export class WorkspaceService extends TypeOrmQueryService { }); if (payload.subdomain && workspace.subdomain !== payload.subdomain) { - await this.validateSubdomainUpdate(payload.subdomain); + await this.subdomainManagerService.validateSubdomainOrThrow( + payload.subdomain, + ); } let customDomainRegistered = false; @@ -202,7 +129,10 @@ export class WorkspaceService extends TypeOrmQueryService { payload.customDomain && workspace.customDomain !== payload.customDomain ) { - await this.setCustomDomain(workspace, payload.customDomain); + await this.customDomainManagerService.setCustomDomain( + workspace, + payload.customDomain, + ); customDomainRegistered = true; } @@ -410,14 +340,6 @@ export class WorkspaceService extends TypeOrmQueryService { } } - async isSubdomainAvailable(subdomain: string) { - const existingWorkspace = await this.workspaceRepository.findOne({ - where: { subdomain: subdomain }, - }); - - return !existingWorkspace; - } - private async validateSecurityPermissions({ payload, userWorkspaceId, @@ -508,45 +430,4 @@ export class WorkspaceService extends TypeOrmQueryService { } } } - - async checkCustomDomainValidRecords( - workspace: Workspace, - domainValidRecord?: DomainValidRecords, - ) { - assertIsDefinedOrThrow(workspace.customDomain); - - const customDomainWithRecords = - domainValidRecord ?? - (await this.dnsManagerService.getHostnameWithRecords( - workspace.customDomain, - )); - - assertIsDefinedOrThrow(customDomainWithRecords); - - const isCustomDomainWorking = - await this.dnsManagerService.isHostnameWorking(workspace.customDomain); - - if (workspace.isCustomDomainEnabled !== isCustomDomainWorking) { - workspace.isCustomDomainEnabled = isCustomDomainWorking; - - await this.workspaceRepository.save(workspace); - - const analytics = this.auditService.createContext({ - workspaceId: workspace.id, - }); - - analytics.insertWorkspaceEvent( - workspace.isCustomDomainEnabled - ? CUSTOM_DOMAIN_ACTIVATED_EVENT - : CUSTOM_DOMAIN_DEACTIVATED_EVENT, - {}, - ); - } - - return customDomainWithRecords; - } - - async findByCustomDomain(customDomain: string) { - return this.workspaceRepository.findOne({ where: { customDomain } }); - } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util.ts b/packages/twenty-server/src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util.ts index 3e7b21dafb..546cb2e738 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util.ts @@ -19,6 +19,7 @@ export const workspaceGraphqlApiExceptionHandler = (error: Error) => { throw new NotFoundError(error); case WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN: case WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN: + case WorkspaceExceptionCode.SUBDOMAIN_NOT_VALID: throw new ConflictError(error); case WorkspaceExceptionCode.ENVIRONMENT_VAR_NOT_ENABLED: case WorkspaceExceptionCode.WORKSPACE_CUSTOM_DOMAIN_DISABLED: diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts index 1d9eacfd84..cff8ca50ba 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.exception.ts @@ -5,6 +5,7 @@ export class WorkspaceException extends CustomException export enum WorkspaceExceptionCode { SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND', SUBDOMAIN_ALREADY_TAKEN = 'SUBDOMAIN_ALREADY_TAKEN', + SUBDOMAIN_NOT_VALID = 'SUBDOMAIN_NOT_VALID', DOMAIN_ALREADY_TAKEN = 'DOMAIN_ALREADY_TAKEN', WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', WORKSPACE_CUSTOM_DOMAIN_DISABLED = 'WORKSPACE_CUSTOM_DOMAIN_DISABLED', diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts index 296b91bdef..c16f1a541b 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.module.ts @@ -10,7 +10,9 @@ import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; import { BillingModule } from 'src/engine/core-modules/billing/billing.module'; import { BillingSubscription } from 'src/engine/core-modules/billing/entities/billing-subscription.entity'; import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { CustomDomainManagerModule } from 'src/engine/core-modules/domain/custom-domain-manager/custom-domain-manager.module'; +import { SubdomainManagerModule } from 'src/engine/core-modules/domain/subdomain-manager/subdomain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; import { FileModule } from 'src/engine/core-modules/file/file.module'; @@ -65,7 +67,9 @@ import { WorkspaceManagerModule } from 'src/engine/workspace-manager/workspace-m RoleModule, AgentModule, DnsManagerModule, - DomainManagerModule, + WorkspaceDomainsModule, + SubdomainManagerModule, + CustomDomainManagerModule, ViewModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, ], 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 37ffb803e0..9aac926d48 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 @@ -25,7 +25,8 @@ import { BillingSubscription } from 'src/engine/core-modules/billing/entities/bi 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 { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-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'; import { FeatureFlagDTO } from 'src/engine/core-modules/feature-flag/dtos/feature-flag-dto'; import { FeatureFlagKey } from 'src/engine/core-modules/feature-flag/enums/feature-flag-key.enum'; import { FeatureFlagService } from 'src/engine/core-modules/feature-flag/services/feature-flag.service'; @@ -89,7 +90,7 @@ const OriginHeader = createParamDecorator( export class WorkspaceResolver { constructor( private readonly workspaceService: WorkspaceService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly userWorkspaceService: UserWorkspaceService, private readonly twentyConfigService: TwentyConfigService, private readonly fileUploadService: FileUploadService, @@ -100,6 +101,7 @@ export class WorkspaceResolver { private readonly agentService: AgentService, private readonly viewService: ViewService, private readonly dnsManagerService: DnsManagerService, + private readonly customDomainManagerService: CustomDomainManagerService, ) {} @Query(() => Workspace) @@ -295,7 +297,7 @@ export class WorkspaceResolver { @ResolveField(() => WorkspaceUrlsDTO) workspaceUrls(@Parent() workspace: Workspace) { - return this.domainManagerService.getWorkspaceUrls(workspace); + return this.workspaceDomainsService.getWorkspaceUrls(workspace); } @ResolveField(() => Boolean) @@ -356,7 +358,7 @@ export class WorkspaceResolver { } const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( origin, ); @@ -379,7 +381,7 @@ export class WorkspaceResolver { id: workspace.id, logo: workspaceLogoWithToken, displayName: workspace.displayName, - workspaceUrls: this.domainManagerService.getWorkspaceUrls(workspace), + workspaceUrls: this.workspaceDomainsService.getWorkspaceUrls(workspace), authProviders: getAuthProvidersByWorkspace({ workspace, systemEnabledProviders, @@ -407,7 +409,7 @@ export class WorkspaceResolver { workspace.customDomain, ); - return this.workspaceService.checkCustomDomainValidRecords( + return this.customDomainManagerService.checkCustomDomainValidRecords( workspace, domainValidRecords, ); diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts index 23fb6642de..e9c0404929 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent-execution.service.ts @@ -17,7 +17,7 @@ import { In } from 'typeorm'; import { getAllSelectableFields } from 'src/engine/api/utils/get-all-selectable-fields.utils'; import { AIBillingService } from 'src/engine/core-modules/ai/services/ai-billing.service'; import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { type Workspace } from 'src/engine/core-modules/workspace/workspace.entity'; import { AgentHandoffToolService } from 'src/engine/metadata-modules/agent/agent-handoff-tool.service'; import { AgentService } from 'src/engine/metadata-modules/agent/agent.service'; @@ -47,7 +47,7 @@ export class AgentExecutionService implements AgentExecutionContext { constructor( private readonly agentHandoffToolService: AgentHandoffToolService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, private readonly twentyORMGlobalManager: TwentyORMGlobalManager, private readonly workspacePermissionsCacheService: WorkspacePermissionsCacheService, private readonly aiModelRegistryService: AiModelRegistryService, @@ -218,7 +218,7 @@ export class AgentExecutionService implements AgentExecutionContext { ).map((record) => { return { ...record, - resourceUrl: this.domainManagerService.buildWorkspaceURL({ + resourceUrl: this.workspaceDomainsService.buildWorkspaceURL({ workspace, pathname: getAppPath(AppPath.RecordShowPage, { objectNameSingular: diff --git a/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts b/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts index bd40799abb..643f88d1d3 100644 --- a/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/agent/agent.module.ts @@ -4,7 +4,7 @@ import { TypeOrmModule } from '@nestjs/typeorm'; import { AiModule } from 'src/engine/core-modules/ai/ai.module'; import { AuditModule } from 'src/engine/core-modules/audit/audit.module'; import { TokenModule } from 'src/engine/core-modules/auth/token/token.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { FeatureFlagModule } from 'src/engine/core-modules/feature-flag/feature-flag.module'; import { FileEntity } from 'src/engine/core-modules/file/entities/file.entity'; import { FileUploadModule } from 'src/engine/core-modules/file/file-upload/file-upload.module'; @@ -68,7 +68,7 @@ import { AgentActorContextService } from './services/agent-actor-context.service WorkspacePermissionsCacheModule, WorkspaceCacheStorageModule, TokenModule, - DomainManagerModule, + WorkspaceDomainsModule, WorkflowToolsModule, UserWorkspaceModule, UserRoleModule, diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.module.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.module.ts index d4ddb63de7..02f3e96381 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.module.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.module.ts @@ -2,7 +2,7 @@ import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AuthModule } from 'src/engine/core-modules/auth/auth.module'; -import { DomainManagerModule } from 'src/engine/core-modules/domain-manager/domain-manager.module'; +import { WorkspaceDomainsModule } from 'src/engine/core-modules/domain/workspace-domains/workspace-domains.module'; import { WorkspaceManyOrAllFlatEntityMapsCacheModule } from 'src/engine/metadata-modules/flat-entity/services/workspace-many-or-all-flat-entity-maps-cache.module'; import { RouteTriggerResolver } from 'src/engine/metadata-modules/route-trigger/resolvers/route-trigger.resolver'; import { RouteTriggerController } from 'src/engine/metadata-modules/route-trigger/route-trigger.controller'; @@ -17,7 +17,7 @@ import { WorkspaceMigrationV2Module } from 'src/engine/workspace-manager/workspa imports: [ TypeOrmModule.forFeature([RouteTrigger]), AuthModule, - DomainManagerModule, + WorkspaceDomainsModule, ServerlessFunctionModule, WorkspaceManyOrAllFlatEntityMapsCacheModule, WorkspaceMigrationV2Module, diff --git a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts index 658c768219..5c1fb90401 100644 --- a/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/route-trigger/route-trigger.service.ts @@ -1,29 +1,29 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { Repository } from 'typeorm'; import { Request } from 'express'; import { match } from 'path-to-regexp'; import { assertIsDefinedOrThrow, isDefined } from 'twenty-shared/utils'; +import { Repository } from 'typeorm'; -import { - HTTPMethod, - RouteTrigger, -} from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; import { AccessTokenService } from 'src/engine/core-modules/auth/token/services/access-token.service'; -import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; -import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service'; +import { WorkspaceDomainsService } from 'src/engine/core-modules/domain/workspace-domains/services/workspace-domains.service'; import { RouteTriggerException, RouteTriggerExceptionCode, } from 'src/engine/metadata-modules/route-trigger/exceptions/route-trigger.exception'; +import { + HTTPMethod, + RouteTrigger, +} from 'src/engine/metadata-modules/route-trigger/route-trigger.entity'; +import { ServerlessFunctionService } from 'src/engine/metadata-modules/serverless-function/serverless-function.service'; @Injectable() export class RouteTriggerService { constructor( private readonly accessTokenService: AccessTokenService, private readonly serverlessFunctionService: ServerlessFunctionService, - private readonly domainManagerService: DomainManagerService, + private readonly workspaceDomainsService: WorkspaceDomainsService, @InjectRepository(RouteTrigger) private readonly routeTriggerRepository: Repository, ) {} @@ -41,7 +41,7 @@ export class RouteTriggerService { const host = `${request.protocol}://${request.get('host')}`; const workspace = - await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace( + await this.workspaceDomainsService.getWorkspaceByOriginOrDefaultWorkspace( host, );