feat(): enable custom domain usage (#9911)

# Content
- Introduce the `workspaceUrls` property. It contains two
sub-properties: `customUrl, subdomainUrl`. These endpoints are used to
access the workspace. Even if the `workspaceUrls` is invalid for
multiple reasons, the `subdomainUrl` remains valid.
- Introduce `ResolveField` workspaceEndpoints to avoid unnecessary URL
computation on the frontend part.
- Add a `forceSubdomainUrl` to avoid custom URL using a query parameter
This commit is contained in:
Antoine Moreaux
2025-02-07 14:34:26 +01:00
committed by GitHub
parent 3cc66fe712
commit 68183b7c85
87 changed files with 645 additions and 373 deletions

View File

@ -9,6 +9,53 @@ import { Workspace } from 'src/engine/core-modules/workspace/workspace.entity';
import { DomainManagerService } from './domain-manager.service';
describe('DomainManagerService', () => {
describe('getworkspaceUrls', () => {
it('should return a URL containing the correct hostname if hostname is provided', () => {
jest
.spyOn(environmentService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONT_PROTOCOL: 'https',
FRONT_DOMAIN: 'example.com',
};
return env[key];
});
const result = domainManagerService.getworkspaceUrls({
subdomain: 'subdomain',
hostname: 'custom-host.com',
});
expect(result).toEqual({
customUrl: 'https://custom-host.com/',
subdomainUrl: 'https://subdomain.example.com/',
});
});
it('should return a URL containing the correct subdomain if hostname is not provided but subdomain is', () => {
jest
.spyOn(environmentService, 'get')
.mockImplementation((key: string) => {
const env = {
FRONT_PROTOCOL: 'https',
FRONT_DOMAIN: 'example.com',
};
return env[key];
});
const result = domainManagerService.getworkspaceUrls({
subdomain: 'subdomain',
hostname: undefined,
});
expect(result).toEqual({
customUrl: undefined,
subdomainUrl: 'https://subdomain.example.com/',
});
});
});
let domainManagerService: DomainManagerService;
let environmentService: EnvironmentService;
@ -106,7 +153,10 @@ describe('DomainManagerService', () => {
});
const result = domainManagerService.buildWorkspaceURL({
subdomain: 'test',
workspace: {
subdomain: 'test',
hostname: undefined,
},
});
expect(result.toString()).toBe('https://test.example.com/');
@ -125,7 +175,10 @@ describe('DomainManagerService', () => {
});
const result = domainManagerService.buildWorkspaceURL({
subdomain: 'subdomain',
workspace: {
subdomain: 'test',
hostname: undefined,
},
pathname: '/path/to/resource',
});
@ -145,8 +198,10 @@ describe('DomainManagerService', () => {
});
const result = domainManagerService.buildWorkspaceURL({
subdomain: 'subdomain',
workspace: {
subdomain: 'test',
hostname: undefined,
},
searchParams: {
foo: 'bar',
baz: 123,

View File

@ -74,33 +74,31 @@ export class DomainManagerService {
buildEmailVerificationURL({
emailVerificationToken,
email,
subdomain,
workspace,
}: {
emailVerificationToken: string;
email: string;
subdomain: string;
workspace: Pick<Workspace, 'subdomain' | 'hostname'>;
}) {
return this.buildWorkspaceURL({
subdomain,
workspace,
pathname: 'verify-email',
searchParams: { emailVerificationToken, email },
});
}
buildWorkspaceURL({
subdomain,
workspace,
pathname,
searchParams,
}: {
subdomain: string;
workspace: Pick<Workspace, 'subdomain' | 'hostname'>;
pathname?: string;
searchParams?: Record<string, string | number>;
}) {
const url = this.getFrontUrl();
const workspaceUrls = this.getworkspaceUrls(workspace);
if (this.environmentService.get('IS_MULTIWORKSPACE_ENABLED')) {
url.hostname = `${subdomain}.${url.hostname}`;
}
const url = new URL(workspaceUrls.customUrl ?? workspaceUrls.subdomainUrl);
if (pathname) {
url.pathname = pathname;
@ -117,21 +115,6 @@ export class DomainManagerService {
return url;
}
// @Deprecated
getWorkspaceSubdomainFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
if (!originHostname.endsWith(this.getFrontUrl().hostname)) {
return null;
}
const frontDomain = this.getFrontUrl().hostname;
const subdomain = originHostname.replace(`.${frontDomain}`, '');
return this.isDefaultSubdomain(subdomain) ? null : subdomain;
};
getSubdomainAndHostnameFromUrl = (url: string) => {
const { hostname: originHostname } = new URL(url);
@ -162,9 +145,12 @@ export class DomainManagerService {
return subdomain === this.environmentService.get('DEFAULT_SUBDOMAIN');
}
computeRedirectErrorUrl(errorMessage: string, subdomain: string) {
computeRedirectErrorUrl(
errorMessage: string,
workspace: Pick<Workspace, 'subdomain' | 'hostname'>,
) {
const url = this.buildWorkspaceURL({
subdomain: subdomain,
workspace,
pathname: '/verify',
searchParams: { errorMessage },
});
@ -352,7 +338,7 @@ export class DomainManagerService {
await this.deleteCustomHostname(fromCustomHostname.id);
}
return await this.registerCustomHostname(toHostname);
return this.registerCustomHostname(toHostname);
}
async deleteCustomHostnameByHostnameSilently(hostname: string) {
@ -378,4 +364,32 @@ export class DomainManagerService {
zone_id: this.environmentService.get('CLOUDFLARE_ZONE_ID'),
});
}
private getCustomWorkspaceEndpoint(hostname: string) {
const url = this.getFrontUrl();
url.hostname = hostname;
return url.toString();
}
private getTwentyWorkspaceEndpoint(subdomain: string) {
const url = this.getFrontUrl();
url.hostname = `${subdomain}.${url.hostname}`;
return url.toString();
}
getworkspaceUrls({
subdomain,
hostname,
}: Pick<Workspace, 'subdomain' | 'hostname'>) {
return {
customUrl: hostname
? this.getCustomWorkspaceEndpoint(hostname)
: undefined,
subdomainUrl: this.getTwentyWorkspaceEndpoint(subdomain),
};
}
}