From f80702120868191a2e4366f572f50719b6b37ef3 Mon Sep 17 00:00:00 2001 From: Antoine Moreaux Date: Mon, 16 Dec 2024 16:27:44 +0100 Subject: [PATCH] WIP --- packages/twenty-server/package.json | 1 + .../environment/environment-variables.ts | 8 +++ .../workspace/dtos/update-workspace-input.ts | 8 +++ .../workspace/services/workspace.service.ts | 67 +++++++++++++++---- .../workspace/workspace.entity.ts | 5 ++ .../workspace/workspace.exception.ts | 1 + yarn.lock | 35 ++++++++++ 7 files changed, 113 insertions(+), 12 deletions(-) diff --git a/packages/twenty-server/package.json b/packages/twenty-server/package.json index 6de9253d6e..cbf3980a64 100644 --- a/packages/twenty-server/package.json +++ b/packages/twenty-server/package.json @@ -30,6 +30,7 @@ "cache-manager": "^5.4.0", "cache-manager-redis-yet": "^4.1.2", "class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch", + "cloudflare": "^3.5.0", "connect-redis": "^7.1.1", "express-session": "^1.18.1", "graphql-middleware": "^6.1.35", diff --git a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts index 687c94c006..a20fd4fd4a 100644 --- a/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/environment/environment-variables.ts @@ -246,6 +246,14 @@ export class EnvironmentVariables { @IsBoolean() IS_MULTIWORKSPACE_ENABLED = false; + @IsString() + @ValidateIf((env) => env.CLOUDFLARE_ZONE_ID) + CLOUDFLARE_USER_SERVICE_KEY: string; + + @IsString() + @ValidateIf((env) => env.CLOUDFLARE_USER_SERVICE_KEY) + CLOUDFLARE_ZONE_ID: string; + // Custom Code Engine @IsEnum(ServerlessDriverType) @IsOptional() 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 19d86e3a3a..d21f702d76 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 @@ -18,6 +18,14 @@ export class UpdateWorkspaceInput { @ForbiddenWords(['demo']) subdomain?: string; + @Field({ nullable: true }) + @IsString() + @IsOptional() + @Matches( + /^(([a-zA-Z0-9]|[a-zA-Z0-9][a-zA-Z0-9-]*[a-zA-Z0-9])\.)*([A-Za-z0-9]|[A-Za-z0-9][A-Za-z0-9-]*[A-Za-z0-9])$/, + ) + domain?: string; + @Field({ nullable: true }) @IsString() @IsOptional() 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 7dcaeab7fd..1f168bdec3 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 @@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm'; import assert from 'assert'; +import Cloudflare from 'cloudflare'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { Repository } from 'typeorm'; @@ -46,6 +47,48 @@ export class WorkspaceService extends TypeOrmQueryService { super(workspaceRepository); } + private async validateSubdomainUpdate(newSubdomain: string) { + const subdomainAvailable = await this.isSubdomainAvailable(newSubdomain); + + if ( + !subdomainAvailable || + this.environmentService.get('DEFAULT_SUBDOMAIN') === newSubdomain + ) { + throw new WorkspaceException( + 'Subdomain already taken', + WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN, + ); + } + } + + async validateDomain(newDomain: string, workspaceId: string) { + const existingWorkspace = await this.isDomainAvailable(newDomain); + + if (existingWorkspace) { + throw new WorkspaceException( + 'Domain already taken', + WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN, + ); + } + + const client = new Cloudflare({ + userServiceKey: this.environmentService.get( + 'CLOUDFLARE_USER_SERVICE_KEY', + ), + }); + + const customHostname = await client.customHostnames.create({ + zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'), + hostname: newDomain, + ssl: {}, + custom_metadata: { + workspaceId, + }, + }); + + console.log('>>>>>>>>>>>>>>', customHostname); + } + async updateWorkspaceById(payload: Partial & { id: string }) { const workspace = await this.workspaceRepository.findOneBy({ id: payload.id, @@ -60,19 +103,11 @@ export class WorkspaceService extends TypeOrmQueryService { ); if (payload.subdomain && workspace.subdomain !== payload.subdomain) { - const subdomainAvailable = await this.isSubdomainAvailable( - payload.subdomain, - ); + await this.validateSubdomainUpdate(payload.subdomain); + } - if ( - !subdomainAvailable || - this.environmentService.get('DEFAULT_SUBDOMAIN') === payload.subdomain - ) { - throw new WorkspaceException( - 'Subdomain already taken', - WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN, - ); - } + if (payload.domain && workspace.domain !== payload.domain) { + await this.validateDomain(payload.domain, workspace.id); } return this.workspaceRepository.save({ @@ -214,4 +249,12 @@ export class WorkspaceService extends TypeOrmQueryService { return !existingWorkspace; } + + async isDomainAvailable(domain: string) { + const existingWorkspace = await this.workspaceRepository.findOne({ + where: { domain }, + }); + + return !existingWorkspace; + } } diff --git a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts index b80ecad5ec..65eea850dc 100644 --- a/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/workspace/workspace.entity.ts @@ -51,6 +51,7 @@ export class Workspace { @PrimaryGeneratedColumn('uuid') id: string; + // @deprecated. Use domain field @Field({ nullable: true }) @Column({ nullable: true }) domainName?: string; @@ -147,6 +148,10 @@ export class Workspace { @Column() subdomain: string; + @Field() + @Column({ unique: true }) + domain: string; + @Field() @Column({ default: true }) isGoogleAuthEnabled: boolean; 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 00d05dfa32..933a4fee94 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 @@ -9,5 +9,6 @@ export class WorkspaceException extends CustomException { export enum WorkspaceExceptionCode { SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND', SUBDOMAIN_ALREADY_TAKEN = 'SUBDOMAIN_ALREADY_TAKEN', + DOMAIN_ALREADY_TAKEN = 'DOMAIN_ALREADY_TAKEN', WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', } diff --git a/yarn.lock b/yarn.lock index a0e934aad7..cab96323b6 100644 --- a/yarn.lock +++ b/yarn.lock @@ -16596,6 +16596,13 @@ __metadata: languageName: node linkType: hard +"@types/qs@npm:^6.9.7": + version: 6.9.17 + resolution: "@types/qs@npm:6.9.17" + checksum: 10c0/a183fa0b3464267f8f421e2d66d960815080e8aab12b9aadab60479ba84183b1cdba8f4eff3c06f76675a8e42fe6a3b1313ea76c74f2885c3e25d32499c17d1b + languageName: node + linkType: hard + "@types/range-parser@npm:*": version: 1.2.7 resolution: "@types/range-parser@npm:1.2.7" @@ -22020,6 +22027,24 @@ __metadata: languageName: node linkType: hard +"cloudflare@npm:^3.5.0": + version: 3.5.0 + resolution: "cloudflare@npm:3.5.0" + dependencies: + "@types/node": "npm:^18.11.18" + "@types/node-fetch": "npm:^2.6.4" + "@types/qs": "npm:^6.9.7" + abort-controller: "npm:^3.0.0" + agentkeepalive: "npm:^4.2.1" + form-data-encoder: "npm:1.7.2" + formdata-node: "npm:^4.3.2" + node-fetch: "npm:^2.6.7" + qs: "npm:^6.10.3" + web-streams-polyfill: "npm:^3.2.1" + checksum: 10c0/bb48ff68a4f5b7e945ceec570e7e17251ad167d8d2dadf8099fd74df46582356382b8cd3f62a9da74cd3a208f8f5181a40c561bc5873592d6a4930458c0e7b86 + languageName: node + linkType: hard + "clsx@npm:^1.1.1, clsx@npm:^1.2.1": version: 1.2.1 resolution: "clsx@npm:1.2.1" @@ -38872,6 +38897,15 @@ __metadata: languageName: node linkType: hard +"qs@npm:^6.10.3": + version: 6.13.1 + resolution: "qs@npm:6.13.1" + dependencies: + side-channel: "npm:^1.0.6" + checksum: 10c0/5ef527c0d62ffca5501322f0832d800ddc78eeb00da3b906f1b260ca0492721f8cdc13ee4b8fd8ac314a6ec37b948798c7b603ccc167e954088df392092f160c + languageName: node + linkType: hard + "qs@npm:~6.5.2": version: 6.5.3 resolution: "qs@npm:6.5.3" @@ -43990,6 +44024,7 @@ __metadata: cache-manager: "npm:^5.4.0" cache-manager-redis-yet: "npm:^4.1.2" class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch" + cloudflare: "npm:^3.5.0" connect-redis: "npm:^7.1.1" express-session: "npm:^1.18.1" graphql-middleware: "npm:^6.1.35"