Skip to content

feat(site): tenant switcher chrome in generated admin shell (#79)#80

Merged
rrrodzilla merged 1 commit into
mainfrom
feat/79-tenant-switcher
May 28, 2026
Merged

feat(site): tenant switcher chrome in generated admin shell (#79)#80
rrrodzilla merged 1 commit into
mainfrom
feat/79-tenant-switcher

Conversation

@rrrodzilla
Copy link
Copy Markdown
Contributor

Closes #79.

Frontend-only — surfaces the operator's tenancy in the generated admin shell and lets multi-tenant users switch the active tenant. Builds on the stateless, header-driven model shipped in #67/#70: the PASETO carries the full tenant_chain; which membership scopes a request is chosen per-request via the X-Active-Tenant header. No active_tenant token claim, no token re-mint, no new backend route (the design decision behind this approach is recorded in the issue discussion).

Changes

  • lib/auth.tsactiveTenantStore (sessionStorage), ACTIVE_TENANT_HEADER, fetchMe(), and MeResponse/TenantRef/ActiveTenant types mirroring the Rust shapes. Active tenant is cleared with the token so a lost session never strands a stale selection.
  • admin/api-client.ts — attach X-Active-Tenant on every request (a caller-supplied header still wins).
  • App.tsxTenantControl in the topbar: picker (≥2 memberships), chip (exactly one), and "Operating as: platform_admin" badge for role holders. Switching stores the choice and hard-reloads to /admin to avoid cross-tenant cache bleed in React Query.
  • site/vendor.rsINDEX_CSS styles for the picker/chip/badge.

Security note

The issue's defense-in-depth requirement ("reject switching to a tenant the caller isn't a member of") is already enforced server-sidetenant_scope returns 403 ACTIVE_TENANT_FORBIDDEN for a non-member header value. This layer is convenience + display, never the boundary.

Verification

  • cargo check -p schema-forge-cli — clean.
  • Generated a site from demo.schema, pnpm install, tsc --noEmitzero new type errors from this change.
  • cargo nextest run -p schema-forge-cli332/332 pass (no render-snapshot regressions).
  • site-e2e (Playwright) will run in CI since this touches templates/site/**.

Out of scope

tsc flags a pre-existing error at App.tsx:541 (toastOptions={{ ariaLive: "polite" }} — not a valid sonner key). Present on main, invisible to CI because the Vite/esbuild path strips types. Left untouched to keep this PR scoped; can fix in a separate fix(site): PR.

Surface the operator's tenancy in the generated admin shell and let
multi-tenant users switch the active tenant, building on the stateless
header-driven model shipped in #67/#70 (no active_tenant token claim;
the active tenant is chosen per-request via the X-Active-Tenant header).

- auth.ts: activeTenantStore (sessionStorage), ACTIVE_TENANT_HEADER,
  fetchMe(), and MeResponse/TenantRef/ActiveTenant types mirroring the
  Rust shapes. The active tenant is cleared alongside the token so a lost
  session never strands a stale selection for the next operator.
- api-client: attach X-Active-Tenant on every request (a caller-supplied
  header still wins). The server already 403s a non-member value, so this
  is convenience + display, never the security boundary.
- App.tsx: TenantControl in the topbar — a picker for >=2 memberships, a
  chip for exactly one, and an "Operating as: platform_admin" badge for
  role holders (the server bypasses tenant scoping for them, so a picker
  would imply a constraint that does not exist). Switching stores the
  choice and hard-reloads to /admin so no prior-tenant data lingers in the
  React Query cache under the new scope.
- vendor.rs: INDEX_CSS styles for the picker, chip, and badge.

Frontend-only; no backend change.
@rrrodzilla rrrodzilla merged commit ef2f2a0 into main May 28, 2026
1 check passed
@rrrodzilla rrrodzilla deleted the feat/79-tenant-switcher branch May 28, 2026 20:01
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

auth+admin: tenant switcher — POST /auth/switch-tenant + active-tenant chrome (split from #71)

1 participant