This commit is contained in:
Antoine Moreaux 2024-12-16 16:27:44 +01:00
parent 2ceb1c87b3
commit f807021208
7 changed files with 113 additions and 12 deletions

View File

@ -30,6 +30,7 @@
"cache-manager": "^5.4.0", "cache-manager": "^5.4.0",
"cache-manager-redis-yet": "^4.1.2", "cache-manager-redis-yet": "^4.1.2",
"class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch", "class-validator": "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch",
"cloudflare": "^3.5.0",
"connect-redis": "^7.1.1", "connect-redis": "^7.1.1",
"express-session": "^1.18.1", "express-session": "^1.18.1",
"graphql-middleware": "^6.1.35", "graphql-middleware": "^6.1.35",

View File

@ -246,6 +246,14 @@ export class EnvironmentVariables {
@IsBoolean() @IsBoolean()
IS_MULTIWORKSPACE_ENABLED = false; 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 // Custom Code Engine
@IsEnum(ServerlessDriverType) @IsEnum(ServerlessDriverType)
@IsOptional() @IsOptional()

View File

@ -18,6 +18,14 @@ export class UpdateWorkspaceInput {
@ForbiddenWords(['demo']) @ForbiddenWords(['demo'])
subdomain?: string; 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 }) @Field({ nullable: true })
@IsString() @IsString()
@IsOptional() @IsOptional()

View File

@ -3,6 +3,7 @@ import { InjectRepository } from '@nestjs/typeorm';
import assert from 'assert'; import assert from 'assert';
import Cloudflare from 'cloudflare';
import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm'; import { TypeOrmQueryService } from '@ptc-org/nestjs-query-typeorm';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
@ -46,6 +47,48 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
super(workspaceRepository); 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<Workspace> & { id: string }) { async updateWorkspaceById(payload: Partial<Workspace> & { id: string }) {
const workspace = await this.workspaceRepository.findOneBy({ const workspace = await this.workspaceRepository.findOneBy({
id: payload.id, id: payload.id,
@ -60,19 +103,11 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
); );
if (payload.subdomain && workspace.subdomain !== payload.subdomain) { if (payload.subdomain && workspace.subdomain !== payload.subdomain) {
const subdomainAvailable = await this.isSubdomainAvailable( await this.validateSubdomainUpdate(payload.subdomain);
payload.subdomain, }
);
if ( if (payload.domain && workspace.domain !== payload.domain) {
!subdomainAvailable || await this.validateDomain(payload.domain, workspace.id);
this.environmentService.get('DEFAULT_SUBDOMAIN') === payload.subdomain
) {
throw new WorkspaceException(
'Subdomain already taken',
WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN,
);
}
} }
return this.workspaceRepository.save({ return this.workspaceRepository.save({
@ -214,4 +249,12 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
return !existingWorkspace; return !existingWorkspace;
} }
async isDomainAvailable(domain: string) {
const existingWorkspace = await this.workspaceRepository.findOne({
where: { domain },
});
return !existingWorkspace;
}
} }

View File

@ -51,6 +51,7 @@ export class Workspace {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
// @deprecated. Use domain field
@Field({ nullable: true }) @Field({ nullable: true })
@Column({ nullable: true }) @Column({ nullable: true })
domainName?: string; domainName?: string;
@ -147,6 +148,10 @@ export class Workspace {
@Column() @Column()
subdomain: string; subdomain: string;
@Field()
@Column({ unique: true })
domain: string;
@Field() @Field()
@Column({ default: true }) @Column({ default: true })
isGoogleAuthEnabled: boolean; isGoogleAuthEnabled: boolean;

View File

@ -9,5 +9,6 @@ export class WorkspaceException extends CustomException {
export enum WorkspaceExceptionCode { export enum WorkspaceExceptionCode {
SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND', SUBDOMAIN_NOT_FOUND = 'SUBDOMAIN_NOT_FOUND',
SUBDOMAIN_ALREADY_TAKEN = 'SUBDOMAIN_ALREADY_TAKEN', SUBDOMAIN_ALREADY_TAKEN = 'SUBDOMAIN_ALREADY_TAKEN',
DOMAIN_ALREADY_TAKEN = 'DOMAIN_ALREADY_TAKEN',
WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND', WORKSPACE_NOT_FOUND = 'WORKSPACE_NOT_FOUND',
} }

View File

@ -16596,6 +16596,13 @@ __metadata:
languageName: node languageName: node
linkType: hard 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:*": "@types/range-parser@npm:*":
version: 1.2.7 version: 1.2.7
resolution: "@types/range-parser@npm:1.2.7" resolution: "@types/range-parser@npm:1.2.7"
@ -22020,6 +22027,24 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "clsx@npm:^1.1.1, clsx@npm:^1.2.1":
version: 1.2.1 version: 1.2.1
resolution: "clsx@npm:1.2.1" resolution: "clsx@npm:1.2.1"
@ -38872,6 +38897,15 @@ __metadata:
languageName: node languageName: node
linkType: hard 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": "qs@npm:~6.5.2":
version: 6.5.3 version: 6.5.3
resolution: "qs@npm:6.5.3" resolution: "qs@npm:6.5.3"
@ -43990,6 +44024,7 @@ __metadata:
cache-manager: "npm:^5.4.0" cache-manager: "npm:^5.4.0"
cache-manager-redis-yet: "npm:^4.1.2" cache-manager-redis-yet: "npm:^4.1.2"
class-validator: "patch:class-validator@0.14.0#./patches/class-validator+0.14.0.patch" 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" connect-redis: "npm:^7.1.1"
express-session: "npm:^1.18.1" express-session: "npm:^1.18.1"
graphql-middleware: "npm:^6.1.35" graphql-middleware: "npm:^6.1.35"