14240 extensibility ability to create multiple custom domains for each workspace 2 (#14307)

Adds a public-domain core-module. 
Reorganize custom-domain files properly
This commit is contained in:
martmull
2025-09-11 12:24:57 +02:00
committed by GitHub
parent ceffc82b9c
commit b84f4075e5
48 changed files with 1215 additions and 797 deletions
@@ -0,0 +1,20 @@
import { Field, ObjectType } from '@nestjs/graphql';
import { IDField } from '@ptc-org/nestjs-query-graphql';
import { UUIDScalarType } from 'src/engine/api/graphql/workspace-schema-builder/graphql-types/scalars';
@ObjectType('PublicDomain')
export class PublicDomainDTO {
@IDField(() => UUIDScalarType)
id: string;
@Field({ nullable: false })
domain: string;
@Field({ nullable: false })
isValidated: boolean;
@Field()
createdAt: Date;
}
@@ -0,0 +1,11 @@
import { Field, ArgsType } from '@nestjs/graphql';
import { IsNotEmpty, IsString } from 'class-validator';
@ArgsType()
export class PublicDomainInput {
@Field(() => String)
@IsString()
@IsNotEmpty()
domain: string;
}
@@ -0,0 +1,22 @@
import { Catch, ExceptionFilter } from '@nestjs/common';
import { assertUnreachable } from 'twenty-shared/utils';
import {
PublicDomainException,
PublicDomainExceptionCode,
} from 'src/engine/core-modules/public-domain/public-domain.exception';
import { UserInputError } from 'src/engine/core-modules/graphql/utils/graphql-errors.util';
@Catch(PublicDomainException)
export class PublicDomainExceptionFilter implements ExceptionFilter {
catch(exception: PublicDomainException) {
switch (exception.code) {
case PublicDomainExceptionCode.PUBLIC_DOMAIN_ALREADY_REGISTERED:
case PublicDomainExceptionCode.DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN:
throw new UserInputError(exception);
default:
assertUnreachable(exception.code);
}
}
}
@@ -0,0 +1,43 @@
import { ObjectType } from '@nestjs/graphql';
import {
Column,
CreateDateColumn,
Entity,
JoinColumn,
ManyToOne,
PrimaryGeneratedColumn,
UpdateDateColumn,
} from 'typeorm';
import { Relation } from 'src/engine/workspace-manager/workspace-sync-metadata/interfaces/relation.interface';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Entity({ name: 'publicDomain', schema: 'core' })
@ObjectType()
export class PublicDomain {
@PrimaryGeneratedColumn('uuid')
id: string;
@CreateDateColumn({ type: 'timestamptz' })
createdAt: Date;
@UpdateDateColumn({ type: 'timestamptz' })
updatedAt: Date;
@Column({ type: 'varchar', nullable: false, unique: true })
domain: string;
@Column({ type: 'boolean', default: false, nullable: false })
isValidated: boolean;
@Column({ nullable: false, type: 'uuid' })
workspaceId: string;
@ManyToOne(() => Workspace, (workspace) => workspace.publicDomains, {
onDelete: 'CASCADE',
})
@JoinColumn({ name: 'workspaceId' })
workspace: Relation<Workspace>;
}
@@ -0,0 +1,8 @@
import { CustomException } from 'src/utils/custom-exception';
export class PublicDomainException extends CustomException<PublicDomainExceptionCode> {}
export enum PublicDomainExceptionCode {
PUBLIC_DOMAIN_ALREADY_REGISTERED = 'PUBLIC_DOMAIN_ALREADY_REGISTERED',
DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN = 'DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN',
}
@@ -0,0 +1,18 @@
import { Module } from '@nestjs/common';
import { NestjsQueryTypeOrmModule } from '@ptc-org/nestjs-query-typeorm';
import { PublicDomainService } from 'src/engine/core-modules/public-domain/public-domain.service';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import { PublicDomainResolver } from 'src/engine/core-modules/public-domain/public-domain.resolver';
import { DnsManagerModule } from 'src/engine/core-modules/dns-manager/dns-manager.module';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@Module({
imports: [
NestjsQueryTypeOrmModule.forFeature([PublicDomain, Workspace]),
DnsManagerModule,
],
providers: [PublicDomainService, PublicDomainResolver],
})
export class PublicDomainModule {}
@@ -0,0 +1,47 @@
import { Args, Mutation, Resolver } from '@nestjs/graphql';
import { UseFilters, UseGuards, UsePipes } from '@nestjs/common';
import { PreventNestToAutoLogGraphqlErrorsFilter } from 'src/engine/core-modules/graphql/filters/prevent-nest-to-auto-log-graphql-errors.filter';
import { WorkspaceAuthGuard } from 'src/engine/guards/workspace-auth.guard';
import { ResolverValidationPipe } from 'src/engine/core-modules/graphql/pipes/resolver-validation.pipe';
import { PublicDomainExceptionFilter } from 'src/engine/core-modules/public-domain/public-domain-exception-filter';
import { PublicDomainService } from 'src/engine/core-modules/public-domain/public-domain.service';
import { PublicDomainDTO } from 'src/engine/core-modules/public-domain/dtos/public-domain.dto';
import { PublicDomainInput } from 'src/engine/core-modules/public-domain/dtos/public-domain.input';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
@UseGuards(WorkspaceAuthGuard)
@UsePipes(ResolverValidationPipe)
@UseFilters(
PublicDomainExceptionFilter,
PreventNestToAutoLogGraphqlErrorsFilter,
)
@Resolver()
export class PublicDomainResolver {
constructor(private readonly publicDomainService: PublicDomainService) {}
@Mutation(() => PublicDomainDTO)
async createPublicDomain(
@Args() { domain }: PublicDomainInput,
@AuthWorkspace() currentWorkspace: Workspace,
): Promise<PublicDomainDTO> {
return this.publicDomainService.createPublicDomain({
domain,
workspace: currentWorkspace,
});
}
@Mutation(() => Boolean)
async deletePublicDomain(
@Args() { domain }: PublicDomainInput,
@AuthWorkspace() currentWorkspace: Workspace,
): Promise<boolean> {
await this.publicDomainService.deletePublicDomain({
domain,
workspace: currentWorkspace,
});
return true;
}
}
@@ -0,0 +1,104 @@
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { t } from '@lingui/core/macro';
import { Repository } from 'typeorm';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { PublicDomainDTO } from 'src/engine/core-modules/public-domain/dtos/public-domain.dto';
import { PublicDomain } from 'src/engine/core-modules/public-domain/public-domain.entity';
import {
PublicDomainException,
PublicDomainExceptionCode,
} from 'src/engine/core-modules/public-domain/public-domain.exception';
import { DnsManagerService } from 'src/engine/core-modules/dns-manager/services/dns-manager.service';
@Injectable()
export class PublicDomainService {
constructor(
private readonly dnsManagerService: DnsManagerService,
@InjectRepository(PublicDomain)
private readonly publicDomainRepository: Repository<PublicDomain>,
@InjectRepository(Workspace)
private readonly workspaceRepository: Repository<Workspace>,
) {}
async deletePublicDomain({
domain,
workspace,
}: {
domain: string;
workspace: Workspace;
}): Promise<void> {
const formattedDomain = domain.trim().toLowerCase();
await this.dnsManagerService.deleteHostnameSilently(formattedDomain, {
isPublicDomain: true,
});
await this.publicDomainRepository.delete({
domain: formattedDomain,
workspaceId: workspace.id,
});
}
async createPublicDomain({
domain,
workspace,
}: {
domain: string;
workspace: Workspace;
}): Promise<PublicDomainDTO> {
const formattedDomain = domain.trim().toLowerCase();
if (
await this.workspaceRepository.findOneBy({
customDomain: formattedDomain,
})
) {
throw new PublicDomainException(
'Domain already used for workspace custom domain',
PublicDomainExceptionCode.DOMAIN_ALREADY_REGISTERED_AS_CUSTOM_DOMAIN,
{
userFriendlyMessage: t`Domain already used for workspace custom domain`,
},
);
}
if (
await this.publicDomainRepository.findOneBy({
domain: formattedDomain,
workspaceId: workspace.id,
})
) {
throw new PublicDomainException(
'Public domain already registered',
PublicDomainExceptionCode.PUBLIC_DOMAIN_ALREADY_REGISTERED,
{
userFriendlyMessage: t`Public domain already registered`,
},
);
}
const publicDomain = this.publicDomainRepository.create({
domain: formattedDomain,
workspaceId: workspace.id,
});
await this.dnsManagerService.registerHostname(formattedDomain, {
isPublicDomain: true,
});
try {
await this.publicDomainRepository.insert(publicDomain);
} catch (error) {
await this.dnsManagerService.deleteHostnameSilently(formattedDomain, {
isPublicDomain: true,
});
throw error;
}
return publicDomain;
}
}