Flows
F 03 Admin Impersonation
Status: ๐ข ready to test Owner: Adilet Last updated: 2026-05-08 Last verified on production: โ
Goal
An admin can temporarily browse the app as another user, to debug what the user sees, troubleshoot a bug report, or spot-check a tester's balance. The session is clearly marked with a banner; impersonation is fully reversible without signing out.
Actors
- Primary โ Admin
- Secondary โ The impersonated target (no notification by design; this is a read-only debug tool)
Preconditions
- Admin is signed in.
- Target user exists.
Trigger
Admin opens the target user's profile (/app/admin/users/:id) and
clicks the "View as user" action.
Happy path โ start impersonation
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | /app/admin/users/:id | Click "View as user" | POST /api/admin/impersonate body { targetUserId } | INSERT INTO admin_impersonation_log | impersonation cookie set | admin.impersonation_started |
| 2 | App | every page | Top banner: "Viewing as ยท End session" | โ | โ | sidebar reflects target's role | โ |
| 3 | Admin | any page | Browse as target โ issues, balance, etc. | (whatever they navigate to) | โ | โ | โ |
Happy path โ end impersonation
| # | Actor | UI surface | Action | API call | DB writes | Side effects | Suggested event |
|---|---|---|---|---|---|---|---|
| 1 | Admin | impersonation banner | Click "End session" | DELETE /api/admin/impersonate | UPDATE admin_impersonation_log SET ended_at = NOW() | impersonation cookie cleared | admin.impersonation_ended |
| 2 | App | banner gone, sidebar reverts | โ | โ | โ | โ | โ |
Acceptance criteria
- AC-1 โ Given the admin clicks "View as user" on a Tester profile, when the page reloads, then the impersonation banner is visible at the top and the sidebar shows the Tester's role-appropriate menu.
- AC-2 โ Given an active impersonation, when the admin navigates to
/app/test-cycles, then they see only the cycles the target is a member of (not all cycles). - AC-3 โ Given an active impersonation, when the admin attempts a destructive write that requires admin role (e.g. POST a tester assignment), then it is blocked โ impersonation grants read parity, not write parity.
- AC-4 โ Given an active impersonation, when the admin clicks "End session", then the cookie is cleared and the next page render shows the admin's own sidebar.
- AC-5 โ Given an admin signs out while impersonating, when the
session ends, then
admin_impersonation_log.ended_atis also set.
Test data / fixtures
- Baseline seed
- At least one tester with at least one filed issue, so impersonation has something to display.
Negative paths
| # | Scenario | Expected behavior |
|---|---|---|
| N-1 | Non-admin calls POST /api/admin/impersonate | 403 |
| N-2 | Admin tries to impersonate themselves | 400 "Cannot impersonate self" |
| N-3 | targetUserId does not exist | 404 |
| N-4 | Two admins impersonate same target simultaneously | Each gets independent cookie + log row; no conflict |
| N-5 | Admin clears cookies manually | Banner disappears, role reverts on next request |
Manual QA checklist
- Open
/app/admin/users/:idfor a tester who has filed issues - Click "View as user" โ banner appears, redirected to
/app - Sidebar shows Tester's items (Test Cycles ยท Balance ยท Browse)
- Navigate to
/app/test-cycles/balanceโ see target's earnings, not your own - Navigate to
/app/test-cyclesโ see only target's cycles - Try to create a new cycle from
/app/admin/test-cycles/newโ should work (admin write parity persists for the admin's own actions, not for target's actions; verify what the policy actually does on prod) - Click "End session" in banner โ banner disappears, sidebar reverts to admin's
- Verify
admin_impersonation_loghas start + end timestamps for the session
Automated test outline
test.describe("F-03 impersonation", () => {
test("admin starts and ends impersonation", async ({ page }) => { โฆ });
test("impersonating reflects target's data", async ({ page }) => { โฆ });
test("self-impersonation blocked", async ({ request }) => { โฆ });
});Code references
- UI:
src/components/test-cycles/ImpersonationBanner.tsx - API:
src/app/api/admin/impersonate/route.ts(POST, DELETE) - Hooks:
src/hooks/test-cycles/impersonation.ts - Auth helper:
src/lib/auth/resolve-user.tsโ reads impersonation cookie - Schema:
admin_impersonation_logtable
Events emitted (proposed)
admin.impersonation_startedโ{ admin_user_id, target_user_id, ip, user_agent }admin.impersonation_endedโ{ admin_user_id, target_user_id, duration_seconds }
Open questions / known gaps
- No "frozen-write" mode flag: writes performed during impersonation are
attributed to the admin in DB columns like
created_by, but the user in the audit trail is the admin (correct behavior โ verify in tests). - No notification to the impersonated user (deliberate โ debug tool).
- Admin sidebar leak fix shipped in #144.