diff --git a/infrastructure/production/fleet/apps/platform-api-config.yaml b/infrastructure/production/fleet/apps/platform-api-config.yaml index 7e087bb..7cdd253 100644 --- a/infrastructure/production/fleet/apps/platform-api-config.yaml +++ b/infrastructure/production/fleet/apps/platform-api-config.yaml @@ -19,9 +19,11 @@ data: STALWART_ADMIN_USER: "admin@dezky.eu" STALWART_PROVISIONING_ENABLED: "true" # Base for per-tenant service mail domains ({slug}.dezky.eu) AND the - # reserved namespace for customer domains: only the dezky tenant may claim - # the apex; nothing under it can be added as a customer domain. + # reserved namespace for customer domains: only the company's own tenant + # (PLATFORM_TENANT_SLUG) may claim the apex; nothing under it can be added + # as a customer domain. PLATFORM_TENANT_DOMAIN: "dezky.eu" + PLATFORM_TENANT_SLUG: "dezky-aps" # JWT validation for portal/operator-issued access tokens. Public Authentik # URLs on purpose: the token `iss` claim is the public URL, and the pod can # hairpin to it through the node's public IP. diff --git a/services/platform-api/src/domains/domains.service.guards.spec.ts b/services/platform-api/src/domains/domains.service.guards.spec.ts index 79e5aa6..167939a 100644 --- a/services/platform-api/src/domains/domains.service.guards.spec.ts +++ b/services/platform-api/src/domains/domains.service.guards.spec.ts @@ -73,10 +73,23 @@ describe('DomainsService.add guards', () => { await expect(svc.add(tenant('dezky'), `mail.${BASE}`, ACTOR)).rejects.toThrow(/reserved/) }) - it('allows the dezky tenant to claim the platform apex', async () => { + it('allows the platform tenant to claim the platform apex', async () => { const { svc, created } = makeService() const view = await svc.add(tenant('dezky'), BASE, ACTOR) expect(view.domain).toBe(BASE) expect(created).toHaveLength(1) }) + + it('respects PLATFORM_TENANT_SLUG for the apex allowance', async () => { + process.env.PLATFORM_TENANT_SLUG = 'dezky-aps' + try { + const { svc, created } = makeService() + await expect(svc.add(tenant('dezky'), BASE, ACTOR)).rejects.toThrow(/reserved/) + const view = await svc.add(tenant('dezky-aps'), BASE, ACTOR) + expect(view.domain).toBe(BASE) + expect(created).toHaveLength(1) + } finally { + delete process.env.PLATFORM_TENANT_SLUG + } + }) }) diff --git a/services/platform-api/src/domains/domains.service.ts b/services/platform-api/src/domains/domains.service.ts index 03c5a6f..6b9d520 100644 --- a/services/platform-api/src/domains/domains.service.ts +++ b/services/platform-api/src/domains/domains.service.ts @@ -81,11 +81,12 @@ export class DomainsService { // The platform's own namespace is reserved. The apex (PLATFORM_TENANT_DOMAIN, // e.g. dezky.eu) doubles as dezky's employee mail domain — only the company's - // own tenant (slug "dezky") may claim it. Everything under it is off limits - // for everyone: that's where per-tenant service domains ({slug}.dezky.eu) - // and the infrastructure hosts (auth/api/app/mail/…) live. + // own tenant (PLATFORM_TENANT_SLUG, prod: dezky-aps) may claim it. Everything + // under it is off limits for everyone: that's where per-tenant service + // domains ({slug}.dezky.eu) and the infrastructure hosts (auth/api/app/mail/…) live. const platformBase = (process.env.PLATFORM_TENANT_DOMAIN || 'dezky.local').toLowerCase() - if (domain === platformBase && tenant.slug !== 'dezky') { + const platformSlug = (process.env.PLATFORM_TENANT_SLUG || 'dezky').toLowerCase() + if (domain === platformBase && tenant.slug !== platformSlug) { throw new ConflictException(`Domain "${domain}" is reserved`) } if (domain.endsWith(`.${platformBase}`)) {