feat(operator): partner-style tenant provisioning wizard + admin invite
ci / tc_portal (push) Has been skipped
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 22s
ci / tc_operator (push) Successful in 24s
ci / build_portal (push) Has been skipped
ci / build_booking (push) Has been skipped
ci / test_platform_api (push) Successful in 32s
ci / build_operator (push) Successful in 31s
ci / build_platform_api (push) Successful in 15s
ci / deploy (push) Successful in 41s

The minimal create modal silently dropped adminName/adminEmail — the invite
only existed in the partner wizard's server path. Operator now gets the
same 5-step wizard UX (organization, domain, first admin, plan with live
price catalog, review) composed client-side: POST /tenants creates +
provisions, then POST /users/invite-tenant-admin (new, operator-only —
lives in UsersModule because UsersModule already imports TenantsModule and
the reverse would be circular) runs the same inviteTenantAdmin flow the
partner gets, and the result view hands over the single-use recovery link
or temp password. Tenant detail page gains an Invite admin action for
retries/successors. PLATFORM_TENANT_SLUG back to 'dezky' (the recreated
company tenant) + config-rev bump to roll platform-api.
This commit is contained in:
Ronni Baslund
2026-06-10 21:22:14 +02:00
parent fb4ff48617
commit 2bc302c082
10 changed files with 1093 additions and 158 deletions
+16
View File
@@ -81,6 +81,10 @@ const INTEGRATION_TONE = {
const INTEGRATIONS = ['authentik', 'stalwart', 'ocis'] as const
type IntegrationKey = (typeof INTEGRATIONS)[number]
// Invite (or re-invite) the tenant's admin — same flow the create wizard
// runs; reusable whenever the first invite failed or someone new takes over.
const inviteAdminOpen = ref(false)
// ── Danger-zone state ─────────────────────────────────────────────────────
const dangerAction = ref<'suspend' | 'resume' | 'delete' | 'purge' | null>(null)
const dangerBusy = ref(false)
@@ -141,6 +145,10 @@ async function reconcile() {
<template #actions>
<Badge :tone="STATUS_TONE[tenant.status]" dot>{{ tenant.status }}</Badge>
<Badge tone="neutral">{{ tenant.plan }}</Badge>
<UiButton variant="secondary" @click="inviteAdminOpen = true">
<template #leading><UiIcon name="plus" :size="13" /></template>
Invite admin
</UiButton>
<UiButton variant="secondary" @click="impersonate.open(tenant)">
<template #leading><UiIcon name="key" :size="13" /></template>
Impersonate
@@ -392,6 +400,14 @@ async function reconcile() {
</p>
<p v-if="dangerError" class="danger-err">{{ dangerError }}</p>
</ConfirmDialog>
<InviteTenantAdminModal
v-if="tenant"
:open="inviteAdminOpen"
:tenant-slug="tenant.slug"
:tenant-name="tenant.name"
@close="inviteAdminOpen = false"
/>
</div>
</template>