Files
techxcar/docs/superpowers/specs/2026-06-29-super-admin-tenant-access.md
Luciano Milani 5de37bb512 Inicial
2026-07-02 12:47:55 +01:00

116 lines
3.7 KiB
Markdown

# Super Admin — Tenant Access Design
**Date:** 2026-06-29
**Status:** Approved
**Scope:** Bug fix (TenantMiddleware schema name) + feature (super admin impersonation with session restore)
---
## Problem
1. **Bug:** `TenantMiddleware` constructs schema name as `"tenant_<uuid-with-hyphens>"` but schemas are named `tenant_<uuid-with-underscores>`. All tenant user requests fail with SET search_path error. Independent of the feature — must always be fixed.
2. **Feature gap:** Super admin can list and create tenants but cannot access or manage data within individual tenant workspaces.
---
## Solution
### Bug Fix — TenantMiddleware
Replace raw UUID concatenation with normalized schema name (hyphens → underscores):
```go
// Before
schema := `"tenant_` + claims.TenantID + `"`
// After
schema := `"tenant_` + strings.ReplaceAll(claims.TenantID, "-", "_") + `"`
```
### Feature — Tenant Impersonation with Session Restore
**Flow:**
1. Super admin clicks "Gerir" on any tenant row in TenantsPage
2. Frontend calls `POST /api/v1/admin/tenants/:id/access`
3. Backend validates tenant (exists + active), generates a 15-min access token with `tenantID=<id>` and `role="tenant_admin"`
4. Frontend saves current session to `previousSession` in authStore, sets new token+user
5. Navigates to `/app` — super admin now sees the tenant workspace
6. AppLayout shows a banner: `[TechXCar Admin] A gerir: <tenant name> [← Voltar ao painel]`
7. Clicking "Voltar" restores the previous session and navigates to `/admin`
---
## Backend
### New endpoint
`POST /api/v1/admin/tenants/:id/access`
- Auth: `RequireAuth` + `RequireRole("super_admin")`
- Validates: tenant ID format, tenant exists, tenant status = "active"
- Returns: `{ "data": { "access_token": "...", "tenant": { "id", "name", "slug" } }, "error": null }`
- Token: standard 15-min access token, `userID = super_admin_id`, `tenantID = tenant.ID`, `role = "tenant_admin"`
- No new refresh token — the impersonation session is access-token only
### Route registration
Added to `tenant.RegisterRoutes` under the existing `admin` group:
```
POST /api/v1/admin/tenants/:id/access
```
---
## Frontend
### authStore changes
New fields:
```typescript
previousSession?: { token: string; user: AuthUser }
```
New actions:
- `impersonateTenant(token, user)` — saves current `{ token, user }` to `previousSession`, sets new token+user
- `restoreSession()` — restores `previousSession` into token+user, clears `previousSession`
`previousSession` is NOT persisted to localStorage (impersonation does not survive page refresh — intentional).
### TenantsPage
Each tenant row gains a "Gerir" button (secondary/outline variant).
On click:
1. POST `/admin/tenants/:id/access`
2. On success: `impersonateTenant(data.access_token, { ...data.tenant, role: 'tenant_admin' })`
3. Invalidate TanStack Query cache (avoid stale super-admin-scoped data)
4. Navigate to `/app`
### AppLayout
When `authStore.previousSession` is defined, render a fixed banner at the top:
```
[TechXCar Admin] A gerir: <user.name> [← Voltar ao painel]
```
- Background: amber/yellow to visually distinguish from normal tenant UI
- "Voltar" button: calls `restoreSession()`, navigates to `/admin`
- Banner height: ~40px; main content padding-top adjusts accordingly
---
## Error Handling
- Tenant not found or not active → 404 "oficina não encontrada ou inativa"
- `impersonateTenant` failure (network) → show error toast, do not change session
- Restoring session never fails (purely client-side state)
---
## Out of Scope
- Refresh token for impersonation sessions (15-min window is intentional)
- Audit log of impersonation events (future)
- Super admin creating/editing data within tenant as themselves (they appear as tenant_admin)