feat(tenants): isPlatformTenant flag replaces PLATFORM_TENANT_SLUG
ci / changes (push) Successful in 4s
ci / tc_portal (push) Has been skipped
ci / tc_booking (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Successful in 22s
ci / tc_operator (push) Successful in 22s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_operator (push) Successful in 30s
ci / test_platform_api (push) Successful in 34s
ci / build_platform_api (push) Successful in 15s
ci / deploy (push) Successful in 42s

Identifying the company tenant by slug in env was fragile — every
purge/recreate changed the slug (or id) and the apex guard chased reality
through three config flips in one day. The identity now lives ON the
tenant document: isPlatformTenant, operator-set from the tenant page
(single holder — setting it clears the flag everywhere else), guarded so
tenant admins can't set it on themselves through the shared PATCH route.
The dezky.eu apex guard reads the flag; PLATFORM_TENANT_SLUG is gone.
Dev seed flags its seeded tenant. config-rev 5 rolls platform-api.
This commit is contained in:
Ronni Baslund
2026-06-10 21:47:27 +02:00
parent eefe1b3ec3
commit 83214eb379
12 changed files with 93 additions and 31 deletions
+35
View File
@@ -85,6 +85,25 @@ type IntegrationKey = (typeof INTEGRATIONS)[number]
// runs; reusable whenever the first invite failed or someone new takes over.
const inviteAdminOpen = ref(false)
// Platform-tenant flag toggle. Single holder — the API clears it from every
// other tenant when set. Grants exactly one thing: claiming the dezky.eu
// apex as this tenant's customer mail domain.
const flagBusy = ref(false)
const flagError = ref<string | null>(null)
async function setPlatformTenant(value: boolean) {
flagBusy.value = true
flagError.value = null
try {
await $fetch(`/api/tenants/${slug.value}`, { method: 'PATCH', body: { isPlatformTenant: value } })
await refreshTenant()
} catch (err: unknown) {
const e = err as { data?: { data?: { message?: string }; message?: string } }
flagError.value = e.data?.data?.message ?? e.data?.message ?? String(err)
} finally {
flagBusy.value = false
}
}
// ── Danger-zone state ─────────────────────────────────────────────────────
const dangerAction = ref<'suspend' | 'resume' | 'delete' | 'purge' | null>(null)
const dangerBusy = ref(false)
@@ -145,6 +164,7 @@ async function reconcile() {
<template #actions>
<Badge :tone="STATUS_TONE[tenant.status]" dot>{{ tenant.status }}</Badge>
<Badge tone="neutral">{{ tenant.plan }}</Badge>
<Badge v-if="tenant.isPlatformTenant" tone="warn">platform tenant</Badge>
<UiButton variant="secondary" @click="inviteAdminOpen = true">
<template #leading><UiIcon name="plus" :size="13" /></template>
Invite admin
@@ -320,6 +340,21 @@ async function reconcile() {
<!-- DANGER (real) -->
<div v-else-if="activeTab === 'danger'" class="grid">
<Card>
<h2>Platform tenant</h2>
<p>
Marks this as dezky's own tenant (single holder — setting it here clears it
anywhere else). Required to claim <Mono>dezky.eu</Mono> as a customer mail
domain; everything else treats it as a normal tenant.
</p>
<UiButton
:variant="tenant.isPlatformTenant ? 'secondary' : 'primary'"
:disabled="flagBusy"
@click="setPlatformTenant(!tenant.isPlatformTenant)"
>{{ tenant.isPlatformTenant ? 'Unmark platform tenant' : 'Mark as platform tenant' }}</UiButton>
<p v-if="flagError" class="danger-err">{{ flagError }}</p>
</Card>
<Card>
<h2 class="danger">Suspend tenant</h2>
<p>