diff --git a/apps/server/package.json b/apps/server/package.json index 63fa0be1..4c66ea46 100644 --- a/apps/server/package.json +++ b/apps/server/package.json @@ -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" diff --git a/apps/server/src/common/middlewares/domain.middleware.ts b/apps/server/src/common/middlewares/domain.middleware.ts index 1a2400b8..4c5ee51d 100644 --- a/apps/server/src/common/middlewares/domain.middleware.ts +++ b/apps/server/src/common/middlewares/domain.middleware.ts @@ -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) { diff --git a/apps/server/src/core/auth/services/auth.service.ts b/apps/server/src/core/auth/services/auth.service.ts index 9c761ef3..c20fc07f 100644 --- a/apps/server/src/core/auth/services/auth.service.ts +++ b/apps/server/src/core/auth/services/auth.service.ts @@ -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, diff --git a/apps/server/src/core/workspace/services/workspace-invitation.service.ts b/apps/server/src/core/workspace/services/workspace-invitation.service.ts index 5ecc8427..e353e058 100644 --- a/apps/server/src/core/workspace/services/workspace-invitation.service.ts +++ b/apps/server/src/core/workspace/services/workspace-invitation.service.ts @@ -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 { - 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 { const inviteLink = await this.buildInviteLink({ invitationId, inviteToken, - hostname, + workspace, }); const emailTemplate = InvitationEmail({ diff --git a/apps/server/src/database/repos/workspace/workspace.repo.ts b/apps/server/src/database/repos/workspace/workspace.repo.ts index 8b9765f4..a97d984c 100644 --- a/apps/server/src/database/repos/workspace/workspace.repo.ts +++ b/apps/server/src/database/repos/workspace/workspace.repo.ts @@ -83,6 +83,14 @@ export class WorkspaceRepo { .executeTakeFirst(); } + async findByCustomDomain(domain: string): Promise { + return await this.db + .selectFrom('workspaces') + .selectAll() + .where(sql`LOWER(custom_domain)`, '=', sql`LOWER(${domain})`) + .executeTakeFirst(); + } + async hostnameExists( hostname: string, trx?: KyselyTransaction, diff --git a/apps/server/src/ee b/apps/server/src/ee index 19197d26..1e127cec 160000 --- a/apps/server/src/ee +++ b/apps/server/src/ee @@ -1 +1 @@ -Subproject commit 19197d261094d58c8e473e9e66a1fc09f6019165 +Subproject commit 1e127cec1d9f47f5ed8bf5a9de017e78a36793ee diff --git a/apps/server/src/integrations/environment/domain.service.ts b/apps/server/src/integrations/environment/domain.service.ts index fa6b1778..ffa02b6e 100644 --- a/apps/server/src/integrations/environment/domain.service.ts +++ b/apps/server/src/integrations/environment/domain.service.ts @@ -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) { diff --git a/apps/server/src/integrations/environment/environment.validation.ts b/apps/server/src/integrations/environment/environment.validation.ts index a2aeb6dd..1b2de44f 100644 --- a/apps/server/src/integrations/environment/environment.validation.ts +++ b/apps/server/src/integrations/environment/environment.validation.ts @@ -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) { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b8c0c88c..882f846c 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -567,6 +567,9 @@ importers: stripe: specifier: ^17.5.0 version: 17.5.0 + tld-extract: + specifier: ^2.1.0 + version: 2.1.0 tmp-promise: specifier: ^3.0.3 version: 3.0.3 @@ -8864,6 +8867,9 @@ packages: tiptap-extension-global-drag-handle@0.1.18: resolution: {integrity: sha512-jwFuy1K8DP3a4bFy76Hpc63w1Sil0B7uZ3mvhQomVvUFCU787Lg2FowNhn7NFzeyok761qY2VG+PZ/FDthWUdg==} + tld-extract@2.1.0: + resolution: {integrity: sha512-Y9QHWIoDQPJJVm3/pOC7kOfOj7vsNSVZl4JGoEHb605FiwZgIfzSMyU0HC0wYw5Cx8435vaG1yGZtIm1yiQGOw==} + tldts-core@6.1.72: resolution: {integrity: sha512-FW3H9aCaGTJ8l8RVCR3EX8GxsxDbQXuwetwwgXA2chYdsX+NY1ytCBl61narjjehWmCw92tc1AxlcY3668CU8g==} @@ -19538,6 +19544,8 @@ snapshots: tiptap-extension-global-drag-handle@0.1.18: {} + tld-extract@2.1.0: {} + tldts-core@6.1.72: {} tldts@6.1.72: