fix(domains): platform tenant slug is configurable (prod: dezky-aps)
ci / changes (push) Successful in 4s
ci / tc_portal (push) Has been skipped
ci / tc_booking (push) Has been skipped
ci / tc_operator (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Successful in 23s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_operator (push) Has been skipped
ci / test_platform_api (push) Successful in 32s
ci / build_platform_api (push) Successful in 18s
ci / deploy (push) Successful in 41s

The company tenant ended up as slug dezky-aps (the seeded 'dezky' tenant was
deleted), so the hardcoded apex allowance for slug 'dezky' would have
rejected adding dezky.eu to the real tenant. PLATFORM_TENANT_SLUG env
(default 'dezky') now names the only tenant allowed to claim the
PLATFORM_TENANT_DOMAIN apex.
This commit is contained in:
Ronni Baslund
2026-06-10 20:57:31 +02:00
parent f66a343472
commit 25d932d3c1
3 changed files with 23 additions and 7 deletions
@@ -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.
@@ -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
}
})
})
@@ -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}`)) {