feat(tenants): hard-delete (purge) for soft-deleted tenants
ci / tc_portal (push) Has been skipped
ci / build_operator (push) Successful in 30s
ci / test_platform_api (push) Successful in 33s
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Successful in 24s
ci / tc_operator (push) Successful in 24s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_platform_api (push) Successful in 15s
ci / deploy (push) Successful in 40s
ci / tc_portal (push) Has been skipped
ci / build_operator (push) Successful in 30s
ci / test_platform_api (push) Successful in 33s
ci / changes (push) Successful in 4s
ci / tc_booking (push) Has been skipped
ci / tc_website (push) Has been skipped
ci / tc_platform_api (push) Successful in 24s
ci / tc_operator (push) Successful in 24s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / build_platform_api (push) Successful in 15s
ci / deploy (push) Successful in 40s
Soft-delete kept the slug occupied forever — no way to remove a test tenant and reuse its name, and external resources lingered. DELETE /tenants/:slug/purge (platform-admin only, two-step: refuses anything not already soft-deleted) tears down the Stalwart service + customer domains (never the platform apex — the management admin account lives there) and the Authentik group, then removes domains/subscriptions/invoices/user links/the tenant doc. Audit trail is kept. Operator detail page shows a 'Purge permanently' card once a tenant is soft-deleted.
This commit is contained in:
@@ -82,7 +82,7 @@ const INTEGRATIONS = ['authentik', 'stalwart', 'ocis'] as const
|
||||
type IntegrationKey = (typeof INTEGRATIONS)[number]
|
||||
|
||||
// ── Danger-zone state ─────────────────────────────────────────────────────
|
||||
const dangerAction = ref<'suspend' | 'resume' | 'delete' | null>(null)
|
||||
const dangerAction = ref<'suspend' | 'resume' | 'delete' | 'purge' | null>(null)
|
||||
const dangerBusy = ref(false)
|
||||
const dangerError = ref<string | null>(null)
|
||||
const reconcileBusy = ref(false)
|
||||
@@ -99,6 +99,11 @@ async function confirmDanger() {
|
||||
await $fetch(`/api/tenants/${slug.value}/resume`, { method: 'POST' })
|
||||
} else if (dangerAction.value === 'delete') {
|
||||
await $fetch(`/api/tenants/${slug.value}`, { method: 'DELETE' })
|
||||
} else if (dangerAction.value === 'purge') {
|
||||
await $fetch(`/api/tenants/${slug.value}/purge`, { method: 'DELETE' })
|
||||
// The tenant no longer exists — back to the list.
|
||||
await navigateTo('/tenants')
|
||||
return
|
||||
}
|
||||
await refreshTenant()
|
||||
dangerAction.value = null
|
||||
@@ -343,14 +348,27 @@ async function reconcile() {
|
||||
@click="dangerAction = 'delete'; dangerError = null"
|
||||
>Soft-delete tenant</UiButton>
|
||||
</Card>
|
||||
|
||||
<Card v-if="tenant.status === 'deleted'">
|
||||
<h2 class="danger">Purge permanently</h2>
|
||||
<p>
|
||||
Tears down external resources (Stalwart domains, Authentik group) and removes
|
||||
every Mongo record except the audit trail. Frees the slug for reuse.
|
||||
Irreversible.
|
||||
</p>
|
||||
<UiButton
|
||||
variant="danger"
|
||||
@click="dangerAction = 'purge'; dangerError = null"
|
||||
>Purge tenant</UiButton>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConfirmDialog
|
||||
:open="dangerAction !== null"
|
||||
:eyebrow="`Tenant · ${tenant.slug}`"
|
||||
:title="dangerAction === 'delete' ? 'Soft-delete this tenant?' : dangerAction === 'suspend' ? 'Suspend this tenant?' : 'Resume this tenant?'"
|
||||
:confirm-label="dangerAction === 'delete' ? 'Delete' : dangerAction === 'suspend' ? 'Suspend' : 'Resume'"
|
||||
:title="dangerAction === 'purge' ? 'Purge this tenant permanently?' : dangerAction === 'delete' ? 'Soft-delete this tenant?' : dangerAction === 'suspend' ? 'Suspend this tenant?' : 'Resume this tenant?'"
|
||||
:confirm-label="dangerAction === 'purge' ? 'Purge forever' : dangerAction === 'delete' ? 'Delete' : dangerAction === 'suspend' ? 'Suspend' : 'Resume'"
|
||||
:tone="dangerAction === 'resume' ? 'primary' : 'danger'"
|
||||
:busy="dangerBusy"
|
||||
@close="dangerAction = null"
|
||||
@@ -363,6 +381,11 @@ async function reconcile() {
|
||||
<p v-else-if="dangerAction === 'resume'">
|
||||
Lift the suspension on <strong>{{ tenant.name }}</strong>. Logins resume immediately.
|
||||
</p>
|
||||
<p v-else-if="dangerAction === 'purge'">
|
||||
Permanently erase <strong>{{ tenant.name }}</strong>: Stalwart domains and the
|
||||
Authentik group are deleted, and all records except the audit trail are removed.
|
||||
This cannot be undone.
|
||||
</p>
|
||||
<p v-else>
|
||||
Mark <strong>{{ tenant.name }}</strong> as deleted. Their data stays in Mongo until
|
||||
the cleanup job runs; external resources stay untouched.
|
||||
|
||||
Reference in New Issue
Block a user