3.7 KiB
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
-
Bug:
TenantMiddlewareconstructs schema name as"tenant_<uuid-with-hyphens>"but schemas are namedtenant_<uuid-with-underscores>. All tenant user requests fail with SET search_path error. Independent of the feature — must always be fixed. -
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):
// Before
schema := `"tenant_` + claims.TenantID + `"`
// After
schema := `"tenant_` + strings.ReplaceAll(claims.TenantID, "-", "_") + `"`
Feature — Tenant Impersonation with Session Restore
Flow:
- Super admin clicks "Gerir" on any tenant row in TenantsPage
- Frontend calls
POST /api/v1/admin/tenants/:id/access - Backend validates tenant (exists + active), generates a 15-min access token with
tenantID=<id>androle="tenant_admin" - Frontend saves current session to
previousSessionin authStore, sets new token+user - Navigates to
/app— super admin now sees the tenant workspace - AppLayout shows a banner:
[TechXCar Admin] A gerir: <tenant name> [← Voltar ao painel] - 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:
previousSession?: { token: string; user: AuthUser }
New actions:
impersonateTenant(token, user)— saves current{ token, user }topreviousSession, sets new token+userrestoreSession()— restorespreviousSessioninto token+user, clearspreviousSession
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:
- POST
/admin/tenants/:id/access - On success:
impersonateTenant(data.access_token, { ...data.tenant, role: 'tenant_admin' }) - Invalidate TanStack Query cache (avoid stale super-admin-scoped data)
- 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"
impersonateTenantfailure (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)