2026-04-29 on prod. Admin-initiated tenant_user impersonation, end-to-end across 5 repos. NIX support clicks "Sign in as" on a tenant user in Cpanel → backend issues a 30-min JWT with an imp claim referencing a new commerce.impersonation_sessions row, sets the shared nix_session cookie on .nixtech.app, returns a redirect URL → admin lands on the tenant subdomain authenticated as the target user, with a sticky amber banner on top of every authed page (Cafe, Commerce, Outdoor) carrying their admin email + an "End impersonation" button. Tenant-side end and admin-side revoke both mark the row ended_at + ended_by, after which the cookie's JWT no longer authenticates anywhere.
impersonation_sessions row, returns sessionId + redirectUrl, sets the shared cookie. DB ground truth confirmed: admin_email, tenant_user_id, ended_at IS NULL./tenant/auth/me with the impersonation cookie returns impersonation: { sessionId, adminEmail }. Cafe's server-rendered auth() sees session.user.impersonation and the layout renders the banner with data-testid="impersonation-banner" + impersonation-admin-email showing admin@nixoutdoor.com./tenant/auth/end-impersonation marks the row ended_at=now, ended_by='tenant'. The row stays for audit but the JWT is rejected on the next call: /me returns 401 with code IMPERSONATION_ENDED./admin/impersonations/:id on a fresh session marks it ended_by='admin'. Active list query returns no live sessions for the tenant after.tenant.impersonation_started and tenant.impersonation_ended rows are written to tenant_audit_logs with the admin id as actor_user_id, the tenant id, and metadata containing session id + duration.admin.nixtech.app with Bearer auth in localStorage; the impersonation cookie on .nixtech.app is a separate scope. End-impersonation only touches the impersonation row + tenant cookie; admin's Cpanel access is untouched.| test-r4-2-prod.mjs (NEW) | 9/9 |
| test-r4-1-prod.mjs | 9/9 |
| test-phase1-prod.mjs | 11/11 |
| test-receipt-preview-prod.mjs | 6/6 |
| test-r2-followups3-prod.mjs | 7/7 |
| test-r2-followups2-prod.mjs | 11/11 |
| test-r2-followups-prod.mjs | 19/19 |
| test-nix-os-r2-4b-prod.mjs | 12/12 |
| test-nix-os-70-2-prod.mjs | 10/10 |
| test-cafe-followups-prod.mjs | 5/5 |
nix-outdoor-sales-backend @ ac9ac06:
migrations/20260429200000_commerce_impersonation_sessions.ts
migrate.js — idempotent entry, ran against
prod Supabase pre-test (created table)
src/admin/impersonation.service.ts — start/end/revoke/list/findActive
src/admin/impersonation.route.ts — POST /admin/tenants/:t/users/:u/impersonate
GET /admin/impersonations/active
DELETE /admin/impersonations/:id
src/tenant/tenant.auth.route.ts — POST /tenant/auth/end-impersonation
/me response surfaces impersonation
src/tenant/tenant.auth.middleware.ts — validates imp claim on every request
src/authentication/auth.middleware.ts — Outdoor's middleware does the same
for /outdoor/* routes
src/authentication/auth.route.ts — /auth/me surfaces impersonation
src/app.ts — mounts admin impersonation router
nix-cafe @ 40a2154:
lib/db/schema.ts — Drizzle schema for the new table
lib/auth.ts — auth() validates session row,
exposes session.user.impersonation
components/impersonation-banner.tsx — server component, sticky amber bar
app/api/auth/end-impersonation/route.ts — Next API route forwards form POST
to backend (cross-origin form workaround)
app/(authed)/layout.tsx — renders banner when impersonating
nix-commerce @ fc86998:
src/stores/auth.js — ssoCookie mode + probeSsoCookie + impersonation
src/main.js — probes cookie before app mount so admin
lands here without localStorage tokens
src/App.vue — banner mounted at root
src/components/ImpersonationBanner.vue — Vue version of the banner
nix-outdoor-sales-frontend @ 176a7c0:
src/stores/authStore.js — already had ssoCookie; gained impersonation
src/layout/AppLayout.vue — banner mounted above layout-wrapper
src/components/ImpersonationBanner.vue — Vue banner
nix-outdoor-sales-admin @ b6b647c (Cpanel):
src/api/client.js — startImpersonation (withCredentials so
browser accepts cross-origin Set-Cookie)
+ listActive + revoke helpers
src/views/TenantDetailView.vue — "Sign in as" button per user row;
opens redirectUrl in a new tab
src/views/ActiveImpersonationsView.vue — NEW — live sessions table + revoke
with 30s auto-refresh
src/router/index.js — /impersonations route
src/layouts/AdminLayout.vue — sidebar entry under Support
D:/NIX Software/:
test-r4-2-prod.mjs — NEW E2E (9 checks)
R4.1 ─ Multi-shop activation E2E ✓ shipped (2026-04-29) R4.x ─ Carried-forward follow-ups bundle ✓ shipped (2026-04-29) R4.2 ─ Impersonation ✓ shipped (2026-04-29) R4.3 ─ Per-product usage endpoint pending (next) R4.4 ─ Drop legacy tenant_limits/plan_code pending (last)