feat(operator): tenant list + 7-tab detail with real lifecycle (O.5)
Operator can now manage tenants end-to-end from the UI:
- pages/tenants/index.vue — list with status/plan/domains/created/
provisioning-state columns, search by slug or name, status chips
with live counts (all/active/pending/suspended), click-through
to detail
- pages/tenants/[slug].vue — 7-tab detail (Overview, Users, Resources,
Billing, Audit, Support, Danger zone)
- 3 tabs hit real backends: Overview (identity + billing fields),
Users (lazy-loaded via new GET /tenants/:slug/users endpoint),
Resources (live provisioning state per integration + Reconcile button)
- 3 tabs render mock fixtures with warn-tone "mock" badges: Billing
(Stripe placeholder), Audit (sample log lines), Support (placeholder
pending the ticket queue work)
- Danger zone: 3 real-backend cards (Suspend / Resume / Soft-delete),
each gated by a ConfirmDialog modal. Verified live — clicked
Suspend on acme, status flipped to 'suspended' in Mongo, then
Resumed back to 'active'
platform-api additions:
- GET /tenants/:slug/users returns users with this tenant in their
tenantIds, sorted by last login. Same authorization rule as the
existing /tenants/:slug — platform admins always pass,
non-admins must be a member of the tenant
- tenants.module imports User schema for the new lookup
New components (apps/operator/components/):
- Tabs.vue — horizontal strip with optional per-tab counts, v-model
- ConfirmDialog.vue — Teleport-to-body modal, Escape/backdrop close,
danger/primary tone for the confirm button
Server proxy infrastructure (apps/operator/server/):
- utils/platform-api.ts — single helper encapsulating
access-token-from-session + bearer-forward + error normalization.
Every operator proxy route is now a one-liner against this helper
- api/tenants/index.get.ts, [slug]/{index.get,index.patch,index.delete,
users.get,suspend.post,resume.post,reconcile.post}.ts
Two real bugs found and fixed during the smoke test:
- Mongoose subdocument `_id` leaks into JSON when iterating
tenant.provisioningStatus. Switched to an explicit
`['authentik', 'stalwart', 'ocis']` whitelist in both v-fors
- Documents created before provisioningErrors was added (like the
acme tenant) don't have the field at all in JSON. Use optional
chaining (`tenant.provisioningErrors?.[k]`) instead of bracket
access. Without it: 'Cannot read properties of undefined (reading
"authentik")' during the Resources tab render
This commit is contained in:
+44
-13
@@ -369,20 +369,51 @@ done in order — earlier ones unblock later ones.
|
||||
needs `allowedHosts: ['operator.dezky.local']` in `nuxt.config.ts`
|
||||
under `vite.server` or every request 403s with a plaintext error.
|
||||
|
||||
### O.5 · Tenant management (real backend)
|
||||
### O.5 · Tenant management (real backend) ✓
|
||||
|
||||
- [ ] `pages/tenants/index.vue` — list with status/plan/seats/MRR columns,
|
||||
filter by partner and status, search by slug/name
|
||||
- [ ] `pages/tenants/[slug].vue` — detail view with tabs
|
||||
- [ ] Tab: **Overview** — header card, key stats, partner link
|
||||
- [ ] Tab: **Users** — list users via `GET /users?tenantSlug=…`
|
||||
- [ ] Tab: **Resources** — provisioning status per integration
|
||||
(Authentik / Stalwart / OCIS), error messages, "Reconcile" button
|
||||
- [ ] Tab: **Billing** (mock fixtures)
|
||||
- [ ] Tab: **Audit** (mock fixtures)
|
||||
- [ ] Tab: **Support** (mock fixtures)
|
||||
- [ ] Tab: **Danger** — suspend, resume, change plan, soft-delete; real
|
||||
backend calls, confirmation modals
|
||||
- [x] `pages/tenants/index.vue` — list with status/plan/domains/created/
|
||||
provisioning-state columns; search by slug or name; status chips
|
||||
(all / active / pending / suspended) with live counts; click-through
|
||||
to detail
|
||||
- [x] `pages/tenants/[slug].vue` — detail with 7 tabs (`Tabs` primitive)
|
||||
- [x] Tab: **Overview** — Identity card + Billing card with key fields
|
||||
- [x] Tab: **Users** — list users via `GET /api/tenants/:slug/users` (new
|
||||
endpoint added to platform-api), lazy-loaded on first tab click
|
||||
- [x] Tab: **Resources** — per-integration `Badge` + external handle
|
||||
(group ID / mail domain / space ID) + last error if any; **Reconcile
|
||||
now** button re-runs orchestration in place. Iterates the explicit
|
||||
`INTEGRATIONS` array, not the raw Mongoose subdoc keys, so the
|
||||
`_id` field doesn't leak into the UI
|
||||
- [x] Tab: **Billing** (mock — plan + MRR + Stripe IDs as fixtures)
|
||||
- [x] Tab: **Audit** (mock — three sample log entries)
|
||||
- [x] Tab: **Support** (mock — placeholder)
|
||||
- [x] Tab: **Danger zone** — three real-backend cards (Suspend / Resume /
|
||||
Soft-delete) each gated by ConfirmDialog. Suspended verified live:
|
||||
acme → `suspended` in Mongo, then Resumed back to `active`
|
||||
|
||||
### Server proxies added (apps/operator/server/api/tenants/)
|
||||
|
||||
`platform-api.ts` util encapsulates the access-token forwarding. Routes:
|
||||
`index.get`, `[slug]/index.{get,patch,delete}`, `[slug]/users.get`,
|
||||
`[slug]/{suspend,resume,reconcile}.post`. All read the operator's session,
|
||||
forward as bearer to platform-api.
|
||||
|
||||
### New primitives this phase
|
||||
|
||||
`Tabs.vue` (horizontal strip with optional counts), `ConfirmDialog.vue`
|
||||
(Teleport-to-body modal, Escape/backdrop close, danger/primary tone).
|
||||
|
||||
### Gotchas worth noting
|
||||
|
||||
- **Mongoose subdoc `_id` leaks into JSON**: iterating `v-for="(state, k)
|
||||
in tenant.provisioningStatus"` includes `_id`. Iterate an explicit
|
||||
whitelist (`['authentik', 'stalwart', 'ocis']`) instead.
|
||||
- **Fields added to schema after document creation are missing in old
|
||||
docs**: acme was created before `provisioningErrors` existed, so
|
||||
`tenant.provisioningErrors[k]` throws `Cannot read properties of
|
||||
undefined`. Use optional chaining (`tenant.provisioningErrors?.[k]`).
|
||||
- Nitro can throw `Could not load /app/server/api/...` if Vite picks
|
||||
up a new file mid-build. Container restart clears it.
|
||||
|
||||
### O.6 · Partner management (real backend)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user