Compare commits

...

1 Commits

Author SHA1 Message Date
Philipinho 62a2eb61ea custom domain support (cloud) 2025-06-28 19:05:03 -07:00
9 changed files with 48 additions and 13 deletions
+1
View File
@@ -82,6 +82,7 @@
"sanitize-filename-ts": "^1.0.2", "sanitize-filename-ts": "^1.0.2",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"stripe": "^17.5.0", "stripe": "^17.5.0",
"tld-extract": "^2.1.0",
"tmp-promise": "^3.0.3", "tmp-promise": "^3.0.3",
"ws": "^8.18.2", "ws": "^8.18.2",
"yauzl": "^3.2.0" "yauzl": "^3.2.0"
@@ -1,4 +1,4 @@
import { Injectable, NestMiddleware, NotFoundException } from '@nestjs/common'; import { Injectable, NestMiddleware } from '@nestjs/common';
import { FastifyRequest, FastifyReply } from 'fastify'; import { FastifyRequest, FastifyReply } from 'fastify';
import { EnvironmentService } from '../../integrations/environment/environment.service'; import { EnvironmentService } from '../../integrations/environment/environment.service';
import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo'; import { WorkspaceRepo } from '@docmost/db/repos/workspace/workspace.repo';
@@ -27,8 +27,19 @@ export class DomainMiddleware implements NestMiddleware {
(req as any).workspace = workspace; (req as any).workspace = workspace;
} else if (this.environmentService.isCloud()) { } else if (this.environmentService.isCloud()) {
const header = req.headers.host; 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); const workspace = await this.workspaceRepo.findByHostname(subdomain);
if (!workspace) { if (!workspace) {
@@ -134,7 +134,7 @@ export class AuthService {
const token = nanoIdGen(16); 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({ await this.userTokenRepo.insertUserToken({
token: token, token: token,
@@ -171,7 +171,7 @@ export class WorkspaceInvitationService {
invitation.email, invitation.email,
invitation.token, invitation.token,
authUser.name, authUser.name,
workspace.hostname, workspace,
); );
}); });
} }
@@ -317,7 +317,7 @@ export class WorkspaceInvitationService {
invitation.email, invitation.email,
invitation.token, invitation.token,
invitedByUser.name, invitedByUser.name,
workspace.hostname, workspace,
); );
} }
@@ -340,17 +340,17 @@ export class WorkspaceInvitationService {
return this.buildInviteLink({ return this.buildInviteLink({
invitationId, invitationId,
inviteToken: token.token, inviteToken: token.token,
hostname: workspace.hostname, workspace: workspace,
}); });
} }
async buildInviteLink(opts: { async buildInviteLink(opts: {
invitationId: string; invitationId: string;
inviteToken: string; inviteToken: string;
hostname?: string; workspace: Workspace;
}): Promise<string> { }): Promise<string> {
const { invitationId, inviteToken, hostname } = opts; const { invitationId, inviteToken, workspace } = opts;
return `${this.domainService.getUrl(hostname)}/invites/${invitationId}?token=${inviteToken}`; return `${this.domainService.getUrl(workspace.hostname, workspace.customDomain)}/invites/${invitationId}?token=${inviteToken}`;
} }
async sendInvitationMail( async sendInvitationMail(
@@ -358,12 +358,12 @@ export class WorkspaceInvitationService {
inviteeEmail: string, inviteeEmail: string,
inviteToken: string, inviteToken: string,
invitedByName: string, invitedByName: string,
hostname?: string, workspace: Workspace,
): Promise<void> { ): Promise<void> {
const inviteLink = await this.buildInviteLink({ const inviteLink = await this.buildInviteLink({
invitationId, invitationId,
inviteToken, inviteToken,
hostname, workspace,
}); });
const emailTemplate = InvitationEmail({ const emailTemplate = InvitationEmail({
@@ -83,6 +83,14 @@ export class WorkspaceRepo {
.executeTakeFirst(); .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( async hostnameExists(
hostname: string, hostname: string,
trx?: KyselyTransaction, trx?: KyselyTransaction,
@@ -5,10 +5,13 @@ import { EnvironmentService } from './environment.service';
export class DomainService { export class DomainService {
constructor(private environmentService: EnvironmentService) {} constructor(private environmentService: EnvironmentService) {}
getUrl(hostname?: string): string { getUrl(hostname?: string, customDomain?: string): string {
if (!this.environmentService.isCloud()) { if (!this.environmentService.isCloud()) {
return this.environmentService.getAppUrl(); return this.environmentService.getAppUrl();
} }
if (customDomain) {
return customDomain;
}
const domain = this.environmentService.getSubdomainHost(); const domain = this.environmentService.getSubdomainHost();
if (!hostname || !domain) { if (!hostname || !domain) {
@@ -68,6 +68,10 @@ export class EnvironmentVariables {
) )
@ValidateIf((obj) => obj.CLOUD === 'true'.toLowerCase()) @ValidateIf((obj) => obj.CLOUD === 'true'.toLowerCase())
SUBDOMAIN_HOST: string; SUBDOMAIN_HOST: string;
@IsOptional()
@ValidateIf((obj) => obj.CLOUD === 'true'.toLowerCase())
APP_IP: string;
} }
export function validate(config: Record<string, any>) { export function validate(config: Record<string, any>) {
+8
View File
@@ -567,6 +567,9 @@ importers:
stripe: stripe:
specifier: ^17.5.0 specifier: ^17.5.0
version: 17.5.0 version: 17.5.0
tld-extract:
specifier: ^2.1.0
version: 2.1.0
tmp-promise: tmp-promise:
specifier: ^3.0.3 specifier: ^3.0.3
version: 3.0.3 version: 3.0.3
@@ -8864,6 +8867,9 @@ packages:
tiptap-extension-global-drag-handle@0.1.18: tiptap-extension-global-drag-handle@0.1.18:
resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==} resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==}
tld-extract@2.1.0:
resolution: {integrity: sha512-Y9QHWIoDQPJJVm3/pOC7kOfOj7vsNSVZl4JGoEHb605FiwZgIfzSMyU0HC0wYw5Cx8435vaG1yGZtIm1yiQGOw==}
tldts-core@6.1.72: tldts-core@6.1.72:
resolution: {integrity: sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==} resolution: {integrity: sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==}
@@ -19538,6 +19544,8 @@ snapshots:
tiptap-extension-global-drag-handle@0.1.18: {} tiptap-extension-global-drag-handle@0.1.18: {}
tld-extract@2.1.0: {}
tldts-core@6.1.72: {} tldts-core@6.1.72: {}
tldts@6.1.72: tldts@6.1.72: