custom domain support (cloud)

This commit is contained in:
Philipinho
2025-06-28 19:05:03 -07:00
parent 232cea8cc9
commit 62a2eb61ea
9 changed files with 48 additions and 13 deletions

View File

@ -82,6 +82,7 @@
"sanitize-filename-ts": "^1.0.2",
"socket.io": "^4.8.1",
"stripe": "^17.5.0",
"tld-extract": "^2.1.0",
"tmp-promise": "^3.0.3",
"ws": "^8.18.2",
"yauzl": "^3.2.0"

View File

@ -1,4 +1,4 @@
import { Injectable, NestMiddleware, NotFoundException } from '@nestjs/common';
import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify';
import { EnvironmentService } from '../../integrations/environment/environment.service';
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
@ -27,8 +27,19 @@ export class DomainMiddleware implements NestMiddleware {
(req as any).workspace = workspace;
} else if (this.environmentService.isCloud()) {
const header = req.headers.host;
const subdomain = header.split('.')[0];
// First, try to find workspace by custom domain
const workspaceByCustomDomain =
await this.workspaceRepo.findByCustomDomain(header);
if (workspaceByCustomDomain) {
(req as any).workspaceId = workspaceByCustomDomain.id;
(req as any).workspace = workspaceByCustomDomain;
return next();
}
// Fall back to subdomain logic
const subdomain = header.split('.')[0];
const workspace = await this.workspaceRepo.findByHostname(subdomain);
if (!workspace) {

View File

@ -134,7 +134,7 @@ export class AuthService {
const token = nanoIdGen(16);
const resetLink = `${this.domainService.getUrl(workspace.hostname)}/password-reset?token=${token}`;
const resetLink = `${this.domainService.getUrl(workspace.hostname, workspace.customDomain)}/password-reset?token=${token}`;
await this.userTokenRepo.insertUserToken({
token: token,

View File

@ -171,7 +171,7 @@ export class WorkspaceInvitationService {
invitation.email,
invitation.token,
authUser.name,
workspace.hostname,
workspace,
);
});
}
@ -317,7 +317,7 @@ export class WorkspaceInvitationService {
invitation.email,
invitation.token,
invitedByUser.name,
workspace.hostname,
workspace,
);
}
@ -340,17 +340,17 @@ export class WorkspaceInvitationService {
return this.buildInviteLink({
invitationId,
inviteToken: token.token,
hostname: workspace.hostname,
workspace: workspace,
});
}
async buildInviteLink(opts: {
invitationId: string;
inviteToken: string;
hostname?: string;
workspace: Workspace;
}): Promise<string> {
const { invitationId, inviteToken, hostname } = opts;
return `${this.domainService.getUrl(hostname)}/invites/${invitationId}?token=${inviteToken}`;
const { invitationId, inviteToken, workspace } = opts;
return `${this.domainService.getUrl(workspace.hostname, workspace.customDomain)}/invites/${invitationId}?token=${inviteToken}`;
}
async sendInvitationMail(
@ -358,12 +358,12 @@ export class WorkspaceInvitationService {
inviteeEmail: string,
inviteToken: string,
invitedByName: string,
hostname?: string,
workspace: Workspace,
): Promise<void> {
const inviteLink = await this.buildInviteLink({
invitationId,
inviteToken,
hostname,
workspace,
});
const emailTemplate = InvitationEmail({

View File

@ -83,6 +83,14 @@ export class WorkspaceRepo {
.executeTakeFirst();
}
async findByCustomDomain(domain: string): Promise<Workspace> {
return await this.db
.selectFrom('workspaces')
.selectAll()
.where(sql`LOWER(custom_domain)`, '=', sql`LOWER(${domain})`)
.executeTakeFirst();
}
async hostnameExists(
hostname: string,
trx?: KyselyTransaction,

View File

@ -5,10 +5,13 @@ import { EnvironmentService } from './environment.service';
export class DomainService {
constructor(private environmentService: EnvironmentService) {}
getUrl(hostname?: string): string {
getUrl(hostname?: string, customDomain?: string): string {
if (!this.environmentService.isCloud()) {
return this.environmentService.getAppUrl();
}
if (customDomain) {
return customDomain;
}
const domain = this.environmentService.getSubdomainHost();
if (!hostname || !domain) {

View File

@ -68,6 +68,10 @@ export class EnvironmentVariables {
)
@ValidateIf((obj) => obj.CLOUD === 'true'.toLowerCase())
SUBDOMAIN_HOST: string;
@IsOptional()
@ValidateIf((obj) => obj.CLOUD === 'true'.toLowerCase())
APP_IP: string;
}
export function validate(config: Record<string, any>) {