feat(custom-domains): allow to register a custom domain (without UI) (#9879)

# In this PR
- Allow to register a custom domain
- Refacto subdomain generation

# In other PRs
- Add UI to deal with a custom domain
- Add logic to work with custom domain
This commit is contained in:
Antoine Moreaux
2025-01-30 13:51:16 +01:00
committed by GitHub
parent ae4bf8d929
commit e895aa27e6
33 changed files with 1049 additions and 240 deletions

View File

@ -37,6 +37,9 @@ export class AvailableWorkspaceOutput {
@Field(() => String)
subdomain: string;
@Field(() => String, { nullable: true })
hostname?: string;
@Field(() => String, { nullable: true })
logo?: string;

View File

@ -7,5 +7,7 @@ export class DomainManagerException extends CustomException {
}
export enum DomainManagerExceptionCode {
CLOUDFLARE_CLIENT_NOT_INITIALIZED = 'CLOUDFLARE_CLIENT_NOT_INITIALIZED',
HOSTNAME_ALREADY_REGISTERED = 'HOSTNAME_ALREADY_REGISTERED',
SUBDOMAIN_REQUIRED = 'SUBDOMAIN_REQUIRED',
}

View File

@ -0,0 +1,75 @@
import { createUnionType, Field, ObjectType } from '@nestjs/graphql';
@ObjectType()
class CustomHostnameOwnershipVerificationTxt {
@Field(() => String)
type: 'txt';
@Field(() => String)
name: string;
@Field(() => String)
value: string;
}
@ObjectType()
class CustomHostnameOwnershipVerificationHttp {
@Field()
type: 'http';
@Field(() => String)
body: string;
@Field(() => String)
url: string;
}
const CustomHostnameOwnershipVerification = createUnionType({
name: 'OwnershipVerification',
types: () =>
[
CustomHostnameOwnershipVerificationTxt,
CustomHostnameOwnershipVerificationHttp,
] as const,
resolveType(value) {
if ('type' in value && value.type === 'txt') {
return CustomHostnameOwnershipVerificationTxt;
}
if ('type' in value && value.type === 'http') {
return CustomHostnameOwnershipVerificationHttp;
}
return null;
},
});
@ObjectType()
export class CustomHostnameDetails {
@Field(() => String)
id: string;
@Field(() => String)
hostname: string;
@Field(() => [CustomHostnameOwnershipVerification])
ownershipVerifications: Array<typeof CustomHostnameOwnershipVerification>;
@Field(() => String, { nullable: true })
status?:
| 'active'
| 'pending'
| 'active_redeploying'
| 'moved'
| 'pending_deletion'
| 'deleted'
| 'pending_blocked'
| 'pending_migration'
| 'pending_provisioned'
| 'test_pending'
| 'test_active'
| 'test_active_apex'
| 'test_blocked'
| 'test_failed'
| 'provisioned'
| 'blocked';
}

View File

@ -2,24 +2,36 @@ import { Injectable, Logger } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import Cloudflare from 'cloudflare';
import { EnvironmentService } from 'src/engine/core-modules/environment/environment.service';
import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import {
WorkspaceException,
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
import { isDefined } from 'src/utils/is-defined';
import { isWorkEmail } from 'src/utils/is-work-email';
import { domainManagerValidator } from 'src/engine/core-modules/domain-manager/validator/cloudflare.validate';
import {
DomainManagerException,
DomainManagerExceptionCode,
} from 'src/engine/core-modules/domain-manager/domain-manager.exception';
import { generateRandomSubdomain } from 'src/engine/core-modules/domain-manager/utils/generate-random-subdomain';
import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name';
import { getSubdomainFromEmail } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-from-email';
import { CustomHostnameDetails } from 'src/engine/core-modules/domain-manager/dtos/custom-hostname-details';
@Injectable()
export class DomainManagerService {
cloudflareClient?: Cloudflare;
constructor(
@InjectRepository(Workspace, 'core')
private readonly workspaceRepository: Repository<Workspace>,
private readonly environmentService: EnvironmentService,
) {}
) {
if (this.environmentService.get('CLOUDFLARE_API_KEY')) {
this.cloudflareClient = new Cloudflare({
apiToken: this.environmentService.get('CLOUDFLARE_API_KEY'),
});
}
}
getFrontUrl() {
let baseUrl: URL;
@ -105,6 +117,7 @@ export class DomainManagerService {
return url;
}
// @Deprecated
getWorkspaceSubdomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
@ -119,6 +132,24 @@ export class DomainManagerService {
return this.isDefaultSubdomain(subdomain) ? null : subdomain;
};
getSubdomainAndHostnameFromUrl = (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,
hostname: isFrontdomain ? undefined : originHostname,
};
};
async getWorkspaceBySubdomainOrDefaultWorkspace(subdomain?: string) {
return subdomain
? await this.workspaceRepository.findOne({
@ -180,121 +211,37 @@ export class DomainManagerService {
}
async getWorkspaceByOriginOrDefaultWorkspace(origin: string) {
try {
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
return this.getDefaultWorkspace();
}
const subdomain = this.getWorkspaceSubdomainFromUrl(origin);
if (!isDefined(subdomain)) return;
return (
(await this.workspaceRepository.findOne({
where: { subdomain },
relations: ['workspaceSSOIdentityProviders'],
})) ?? undefined
);
} catch (e) {
throw new WorkspaceException(
'Workspace not found',
WorkspaceExceptionCode.SUBDOMAIN_NOT_FOUND,
);
if (!this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
return this.getDefaultWorkspace();
}
const { subdomain, hostname } = this.getSubdomainAndHostnameFromUrl(origin);
if (!hostname && !subdomain) return;
const where = isDefined(hostname) ? { hostname } : { subdomain };
return (
(await this.workspaceRepository.findOne({
where,
relations: ['workspaceSSOIdentityProviders'],
})) ?? undefined
);
}
private generateRandomSubdomain(): string {
const prefixes = [
'cool',
'smart',
'fast',
'bright',
'shiny',
'happy',
'funny',
'clever',
'brave',
'kind',
'gentle',
'quick',
'sharp',
'calm',
'silent',
'lucky',
'fierce',
'swift',
'mighty',
'noble',
'bold',
'wise',
'eager',
'joyful',
'glad',
'zany',
'witty',
'bouncy',
'graceful',
'colorful',
];
const suffixes = [
'raccoon',
'panda',
'whale',
'tiger',
'dolphin',
'eagle',
'penguin',
'owl',
'fox',
'wolf',
'lion',
'bear',
'hawk',
'shark',
'sparrow',
'moose',
'lynx',
'falcon',
'rabbit',
'hedgehog',
'monkey',
'horse',
'koala',
'kangaroo',
'elephant',
'giraffe',
'panther',
'crocodile',
'seal',
'octopus',
];
private extractSubdomain(params?: { email?: string; displayName?: string }) {
if (params?.email) {
return getSubdomainFromEmail(params.email);
}
const randomPrefix = prefixes[Math.floor(Math.random() * prefixes.length)];
const randomSuffix = suffixes[Math.floor(Math.random() * suffixes.length)];
return `${randomPrefix}-${randomSuffix}`;
}
private getSubdomainNameByEmail(email?: string) {
if (!isDefined(email) || !isWorkEmail(email)) return;
return getDomainNameByEmail(email);
}
private getSubdomainNameByDisplayName(displayName?: string) {
if (!isDefined(displayName)) return;
const displayNameWords = displayName.match(/(\w| |\d)+/g);
if (displayNameWords) {
return displayNameWords.join('-').replace(/ /g, '').toLowerCase();
if (params?.displayName) {
return getSubdomainNameFromDisplayName(params.displayName);
}
}
async generateSubdomain(params?: { email?: string; displayName?: string }) {
const subdomain =
this.getSubdomainNameByEmail(params?.email) ??
this.getSubdomainNameByDisplayName(params?.displayName) ??
this.generateRandomSubdomain();
this.extractSubdomain(params) ?? generateRandomSubdomain();
const existingWorkspaceCount = await this.workspaceRepository.countBy({
subdomain,
@ -302,4 +249,133 @@ export class DomainManagerService {
return `${subdomain}${existingWorkspaceCount > 0 ? `-${Math.random().toString(36).substring(2, 10)}` : ''}`;
}
async registerCustomHostname(hostname: string) {
domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
if (await this.getCustomHostnameDetails(hostname)) {
throw new DomainManagerException(
'Hostname already registered',
DomainManagerExceptionCode.HOSTNAME_ALREADY_REGISTERED,
);
}
return await this.cloudflareClient.customHostnames.create({
zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'),
hostname,
ssl: {
method: 'txt',
type: 'dv',
settings: {
http2: 'on',
min_tls_version: '1.2',
tls_1_3: 'on',
ciphers: ['ECDHE-RSA-AES128-GCM-SHA256', 'AES128-SHA'],
early_hints: 'on',
},
bundle_method: 'ubiquitous',
wildcard: false,
},
});
}
async getCustomHostnameDetails(
hostname: string,
): Promise<CustomHostnameDetails | undefined> {
domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
const response = await this.cloudflareClient.customHostnames.list({
zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'),
hostname,
});
if (response.result.length === 0) {
return undefined;
}
if (response.result.length === 1) {
return {
id: response.result[0].id,
hostname: response.result[0].hostname,
status: response.result[0].status,
ownershipVerifications: [
response.result[0].ownership_verification,
response.result[0].ownership_verification_http,
].reduce(
(acc, ownershipVerification) => {
if (!ownershipVerification) return acc;
if (
'http_body' in ownershipVerification &&
'http_url' in ownershipVerification &&
ownershipVerification.http_body &&
ownershipVerification.http_url
) {
acc.push({
type: 'http',
body: ownershipVerification.http_body,
url: ownershipVerification.http_url,
});
}
if (
'type' in ownershipVerification &&
ownershipVerification.type === 'txt' &&
ownershipVerification.value &&
ownershipVerification.name
) {
acc.push({
type: 'txt',
value: ownershipVerification.value,
name: ownershipVerification.name,
});
}
return acc;
},
[] as CustomHostnameDetails['ownershipVerifications'],
),
};
}
// should never append. error 5xx
throw new Error('More than one custom hostname found in cloudflare');
}
async updateCustomHostname(fromHostname: string, toHostname: string) {
domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
const fromCustomHostname =
await this.getCustomHostnameDetails(fromHostname);
if (fromCustomHostname) {
await this.deleteCustomHostname(fromCustomHostname.id);
}
return await this.registerCustomHostname(toHostname);
}
async deleteCustomHostnameByHostnameSilently(hostname: string) {
domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
try {
const customHostname = await this.getCustomHostnameDetails(hostname);
if (customHostname) {
await this.cloudflareClient.customHostnames.delete(customHostname.id, {
zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'),
});
}
} catch (err) {
return;
}
}
async deleteCustomHostname(customHostnameId: string) {
domainManagerValidator.isCloudflareInstanceDefined(this.cloudflareClient);
return this.cloudflareClient.customHostnames.delete(customHostnameId, {
zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'),
});
}
}

View File

@ -0,0 +1,16 @@
import { generateRandomSubdomain } from 'src/engine/core-modules/domain-manager/utils/generate-random-subdomain';
describe('generateRandomSubdomain', () => {
it('should return a string in the format "prefix-suffix"', () => {
const result = generateRandomSubdomain();
expect(result).toMatch(/^[a-z]+-[a-z]+$/);
});
it('should generate different results on consecutive calls', () => {
const result1 = generateRandomSubdomain();
const result2 = generateRandomSubdomain();
expect(result1).not.toEqual(result2);
});
});

View File

@ -0,0 +1,25 @@
import { getSubdomainFromEmail } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-from-email';
describe('getSubdomainFromEmail', () => {
beforeEach(() => {
jest.clearAllMocks();
});
it('should return undefined if email is not defined', () => {
const result = getSubdomainFromEmail(undefined);
expect(result).toBeUndefined();
});
it('should return undefined if email is not a work email', () => {
const result = getSubdomainFromEmail('test@gmail.com');
expect(result).toBeUndefined();
});
it('should return the domain name if email is valid and a work email', () => {
const result = getSubdomainFromEmail('test@twenty.com');
expect(result).toBe('twenty');
});
});

View File

@ -0,0 +1,41 @@
import { getSubdomainNameFromDisplayName } from 'src/engine/core-modules/domain-manager/utils/get-subdomain-name-from-display-name';
describe('getSubdomainNameFromDisplayName', () => {
it('should return a hyphen-separated, lowercase subdomain name without spaces for a valid display name', () => {
const result = getSubdomainNameFromDisplayName('My Display Name 123');
expect(result).toBe('my-display-name-123');
});
it('should return undefined if displayName is undefined', () => {
const result = getSubdomainNameFromDisplayName(undefined);
expect(result).toBeUndefined();
});
it('should handle display names with special characters by removing them but keeping words and numbers', () => {
const result = getSubdomainNameFromDisplayName('Hello!@# World$%^ 2023');
expect(result).toBe('hello-world-2023');
});
it('should return a single word in lowercase if displayName consists of one valid word', () => {
const result = getSubdomainNameFromDisplayName('SingleWord');
expect(result).toBe('singleword');
});
it('should return undefined when displayName contains only special characters', () => {
const result = getSubdomainNameFromDisplayName('!@#$%^&*()');
expect(result).toBeUndefined();
});
it('should handle display names with multiple spaces by removing them', () => {
const result = getSubdomainNameFromDisplayName(
' Spaced Out Name ',
);
expect(result).toBe('spaced-out-name');
});
});

View File

@ -0,0 +1,71 @@
export const generateRandomSubdomain = () => {
const prefixes = [
'cool',
'smart',
'fast',
'bright',
'shiny',
'happy',
'funny',
'clever',
'brave',
'kind',
'gentle',
'quick',
'sharp',
'calm',
'silent',
'lucky',
'fierce',
'swift',
'mighty',
'noble',
'bold',
'wise',
'eager',
'joyful',
'glad',
'zany',
'witty',
'bouncy',
'graceful',
'colorful',
];
const suffixes = [
'raccoon',
'panda',
'whale',
'tiger',
'dolphin',
'eagle',
'penguin',
'owl',
'fox',
'wolf',
'lion',
'bear',
'hawk',
'shark',
'sparrow',
'moose',
'lynx',
'falcon',
'rabbit',
'hedgehog',
'monkey',
'horse',
'koala',
'kangaroo',
'elephant',
'giraffe',
'panther',
'crocodile',
'seal',
'octopus',
];
const randomPrefix = prefixes[Math.floor(Math.random() * prefixes.length)];
const randomSuffix = suffixes[Math.floor(Math.random() * suffixes.length)];
return `${randomPrefix}-${randomSuffix}`;
};

View File

@ -0,0 +1,11 @@
import { isDefined } from 'src/utils/is-defined';
import { isWorkEmail } from 'src/utils/is-work-email';
import { getDomainNameByEmail } from 'src/utils/get-domain-name-by-email';
export const getSubdomainFromEmail = (email?: string) => {
if (!isDefined(email) || !isWorkEmail(email)) return;
const domain = getDomainNameByEmail(email);
return domain.split('.')[0];
};

View File

@ -0,0 +1,10 @@
import { isDefined } from 'src/utils/is-defined';
export const getSubdomainNameFromDisplayName = (displayName?: string) => {
if (!isDefined(displayName)) return;
const displayNameWords = displayName.match(/(\w|\d)+/g);
if (displayNameWords) {
return displayNameWords.join('-').replace(/ /g, '').toLowerCase();
}
};

View File

@ -0,0 +1,23 @@
import Cloudflare from 'cloudflare';
import {
DomainManagerException,
DomainManagerExceptionCode,
} from 'src/engine/core-modules/domain-manager/domain-manager.exception';
const isCloudflareInstanceDefined = (
cloudflareInstance: Cloudflare | undefined | null,
): asserts cloudflareInstance is Cloudflare => {
if (!cloudflareInstance) {
throw new DomainManagerException(
'Cloudflare instance is not defined',
DomainManagerExceptionCode.CLOUDFLARE_CLIENT_NOT_INITIALIZED,
);
}
};
export const domainManagerValidator: {
isCloudflareInstanceDefined: typeof isCloudflareInstanceDefined;
} = {
isCloudflareInstanceDefined,
};

View File

@ -242,6 +242,14 @@ export class EnvironmentVariables {
@IsBoolean()
IS_MULTIWORKSPACE_ENABLED = false;
@IsString()
@ValidateIf((env) => env.CLOUDFLARE_ZONE_ID)
CLOUDFLARE_API_KEY: string;
@IsString()
@ValidateIf((env) => env.CLOUDFLARE_API_KEY)
CLOUDFLARE_ZONE_ID: string;
// Custom Code Engine
@IsEnum(ServerlessDriverType)
@IsOptional()

View File

@ -58,4 +58,7 @@ export class PublicWorkspaceDataOutput {
@Field(() => String)
subdomain: Workspace['subdomain'];
@Field(() => String, { nullable: true })
hostname: Workspace['hostname'];
}

View File

@ -10,11 +10,6 @@ import {
@InputType()
export class UpdateWorkspaceInput {
@Field({ nullable: true })
@IsString()
@IsOptional()
domainName?: string;
@Field({ nullable: true })
@IsString()
@IsOptional()
@ -105,6 +100,14 @@ export class UpdateWorkspaceInput {
])
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])$/,
)
hostname?: string;
@Field({ nullable: true })
@IsString()
@IsOptional()

View File

@ -23,6 +23,8 @@ import {
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { WorkspaceManagerService } from 'src/engine/workspace-manager/workspace-manager.service';
import { DEFAULT_FEATURE_FLAGS } from 'src/engine/workspace-manager/workspace-sync-metadata/constants/default-feature-flags';
import { isDefined } from 'src/utils/is-defined';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
@Injectable()
// eslint-disable-next-line @nx/workspace-inject-workspace-repository
@ -40,43 +42,96 @@ export class WorkspaceService extends TypeOrmQueryService<Workspace> {
private readonly billingService: BillingService,
private readonly userWorkspaceService: UserWorkspaceService,
private readonly environmentService: EnvironmentService,
private readonly domainManagerService: DomainManagerService,
) {
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,
);
}
}
private async setCustomDomain(workspace: Workspace, hostname: string) {
const existingWorkspace = await this.workspaceRepository.findOne({
where: { hostname },
});
if (existingWorkspace && existingWorkspace.id !== workspace.id) {
throw new WorkspaceException(
'Domain already taken',
WorkspaceExceptionCode.DOMAIN_ALREADY_TAKEN,
);
}
if (
hostname &&
workspace.hostname !== hostname &&
isDefined(workspace.hostname)
) {
await this.domainManagerService.updateCustomHostname(
workspace.hostname,
hostname,
);
}
if (
hostname &&
workspace.hostname !== hostname &&
!isDefined(workspace.hostname)
) {
await this.domainManagerService.registerCustomHostname(hostname);
}
}
async updateWorkspaceById(payload: Partial<Workspace> & { id: string }) {
const workspace = await this.workspaceRepository.findOneBy({
id: payload.id,
});
workspaceValidator.assertIsDefinedOrThrow(
workspace,
new WorkspaceException(
'Workspace not found',
WorkspaceExceptionCode.WORKSPACE_NOT_FOUND,
),
);
workspaceValidator.assertIsDefinedOrThrow(workspace);
if (payload.subdomain && workspace.subdomain !== payload.subdomain) {
const subdomainAvailable = await this.isSubdomainAvailable(
payload.subdomain,
);
if (
!subdomainAvailable ||
this.environmentService.get('DEFAULT_SUBDOMAIN') === payload.subdomain
) {
throw new WorkspaceException(
'Subdomain already taken',
WorkspaceExceptionCode.SUBDOMAIN_ALREADY_TAKEN,
);
}
await this.validateSubdomainUpdate(payload.subdomain);
}
return this.workspaceRepository.save({
...workspace,
...payload,
});
let customDomainRegistered = false;
if (payload.hostname === null && isDefined(workspace.hostname)) {
await this.domainManagerService.deleteCustomHostnameByHostnameSilently(
workspace.hostname,
);
}
if (payload.hostname && workspace.hostname !== payload.hostname) {
await this.setCustomDomain(workspace, payload.hostname);
customDomainRegistered = true;
}
try {
return await this.workspaceRepository.save({
...workspace,
...payload,
});
} catch (error) {
// revert custom domain registration on error
if (payload.hostname && customDomainRegistered) {
this.domainManagerService
.deleteCustomHostnameByHostnameSilently(payload.hostname)
.catch(() => {
// send to sentry
});
}
}
}
async activateWorkspace(

View File

@ -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',
}

View File

@ -33,10 +33,6 @@ import {
import { UpdateWorkspaceInput } from 'src/engine/core-modules/workspace/dtos/update-workspace-input';
import { getAuthProvidersByWorkspace } from 'src/engine/core-modules/workspace/utils/get-auth-providers-by-workspace.util';
import { workspaceGraphqlApiExceptionHandler } from 'src/engine/core-modules/workspace/utils/workspace-graphql-api-exception-handler.util';
import {
WorkspaceException,
WorkspaceExceptionCode,
} from 'src/engine/core-modules/workspace/workspace.exception';
import { workspaceValidator } from 'src/engine/core-modules/workspace/workspace.validate';
import { AuthUser } from 'src/engine/decorators/auth/auth-user.decorator';
import { AuthWorkspace } from 'src/engine/decorators/auth/auth-workspace.decorator';
@ -48,6 +44,7 @@ import { GraphqlValidationExceptionFilter } from 'src/filters/graphql-validation
import { assert } from 'src/utils/assert';
import { isDefined } from 'src/utils/is-defined';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
import { CustomHostnameDetails } from 'src/engine/core-modules/domain-manager/dtos/custom-hostname-details';
import { Workspace } from './workspace.entity';
@ -218,21 +215,27 @@ export class WorkspaceResolver {
return isDefined(this.environmentService.get('ENTERPRISE_KEY'));
}
@Query(() => CustomHostnameDetails, { nullable: true })
@UseGuards(WorkspaceAuthGuard)
async getHostnameDetails(
@AuthWorkspace() { hostname }: Workspace,
): Promise<CustomHostnameDetails | undefined> {
if (!hostname) return undefined;
return await this.domainManagerService.getCustomHostnameDetails(hostname);
}
@Query(() => PublicWorkspaceDataOutput)
async getPublicWorkspaceDataBySubdomain(@OriginHeader() origin: string) {
async getPublicWorkspaceDataBySubdomain(
@OriginHeader() origin: string,
): Promise<PublicWorkspaceDataOutput | undefined> {
try {
const workspace =
await this.domainManagerService.getWorkspaceByOriginOrDefaultWorkspace(
origin,
);
workspaceValidator.assertIsDefinedOrThrow(
workspace,
new WorkspaceException(
'Workspace not found',
WorkspaceExceptionCode.WORKSPACE_NOT_FOUND,
),
);
workspaceValidator.assertIsDefinedOrThrow(workspace);
let workspaceLogoWithToken = '';
@ -261,6 +264,7 @@ export class WorkspaceResolver {
logo: workspaceLogoWithToken,
displayName: workspace.displayName,
subdomain: workspace.subdomain,
hostname: workspace.hostname,
authProviders: getAuthProvidersByWorkspace({
workspace,
systemEnabledProviders,