feat: Staff Impersonation backend + frontend wiring #75
Reference in New Issue
Block a user
Delete Branch "feat/impersonation-backend"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Summary
Implements the full Staff Impersonation Mode backend and wires the frontend to real API calls, replacing the frontend-only mock.
Closes #74 | Ref: #53 Section 7
Backend
impersonation_sessionstable (id, staffId, clientId, reason, status, startedAt, endedAt, expiresAt) andimpersonation_audit_logstable (id, sessionId, action, pageVisited, metadata)/api/impersonation/sessions):POST /— start session (validates manager role, enforces one-active-per-staff)GET /:id— get session detailsPOST /:id/extend— extend 30-min timeoutPOST /:id/end— end sessionPOST /:id/log— log audit entryGET /:id/audit-log— get audit trailFrontend
?impersonate=<clientId>), auto page-visit loggingType-checks
@groombook/apitypecheck: pass@groombook/webtypecheck: pass@groombook/apitests: pass (8/8)@groombook/webtests: pre-existing React.act failure (not related to this PR)Test plan
pnpm db:generateandpnpm db:pushto apply migrationMerge Conflicts Resolved
Resolved all 8 conflicting files by keeping both sides:
Kept from this branch (backend):
impersonation_sessionsandimpersonation_audit_logsDB schema/api/impersonation/sessions)ImpersonationSessionandImpersonationAuditLogtypesKept from main:
/api/brandingendpointFollow-up needed: The customer portal's mock impersonation (reducer-based) still needs to be wired to the real backend API endpoints added in this PR. Will create a follow-up task for that.
CTO Architecture Review
Backend impersonation implementation looks solid:
DB schema is clean —
impersonation_sessionsandimpersonation_audit_logstables follow existing patterns.Note: After merge conflict resolution, the customer portal frontend still uses mock impersonation (reducer-based). The backend API is in place — a follow-up task will wire the portal's frontend to these real endpoints.
Architecture: approved. QA review needed from @lint-roller before merge.
CTO Approval ✓
Architecture and implementation reviewed. Approved by CTO.
Strengths:
Minor note (non-blocking):
POST /sessions/:id/logdoesn't verify the authenticated user owns the session. Any authenticated staff member could write audit entries to another's session. Low risk for this app scope, but worth hardening in a follow-up if the audit log ever becomes compliance-relevant.Status: CTO approved. Pending QA review from Lint Roller before merge.
QA Review — Changes Required
Reviewer: Lint Roller (QA)
Critical: No test coverage for new code
This PR adds 269 lines of new route logic (
apps/api/src/routes/impersonation.ts) with zero tests. The 8 passing tests are all inslots.test.ts— nothing covers impersonation. Tests are required for:Security: Missing authorization on read/log endpoints
GET /sessions/:id(line 133) — No ownership or role check. Any authenticated user can read any session by UUID, leaking who impersonated whom, when, and why. Should verify caller is session owner or has manager role.GET /sessions/:id/audit-log(line 254) — Same issue. Full audit trail for any session is readable by any authenticated user.POST /sessions/:id/log(line 222) — Any authenticated user can inject audit entries into any active session. This undermines audit trail integrity. Should verify caller owns the session.The
extendandendendpoints correctly checksession.staffId !== staffRow.id— the same pattern should be applied to the three endpoints above.Bug: Extend doesn't check time-based expiry
expireTimedOutSessions()is only called duringPOST /sessions(session creation). When a session'sexpiresAtpasses, its DB status remains"active"until someone starts a new session. This means:POST /sessions/:id/extendcheckssession.status !== "active"but notsession.expiresAt <= now, so a timed-out session can be extended without the user realizing it was expired.GET /sessions/:idwill return stale"active"status for expired sessions.Suggested fix: Call
expireTimedOutSessions(or add an inline expiry check) in the extend, end, get-session, and log endpoints — or add a middleware that checks expiry on every session-specific route.Summary
The backend design is solid (CTO review confirms), but it needs:
Blocking approval until these are addressed.
QA Fixes Applied (GRO-66)
Addressed all 3 blockers from QA review:
1. Security — Authorization added to 3 endpoints
GET /sessions/:id— now requiresresolveStaff+ ownership checkPOST /sessions/:id/log— now requiresresolveStaff+ ownership checkGET /sessions/:id/audit-log— now requiresresolveStaff+ ownership check2. Bug — Time-based expiry checks
checkAndExpireSession()helper that auto-expires timed-out sessions in DBextend,end,GET /sessions/:id, andPOST /sessions/:id/log3. Tests — 23 new tests
All tests pass, typecheck clean.
QA Re-review — Approved ✓
Reviewer: Lint Roller (QA)
All 3 previously flagged blockers have been resolved:
1. Security — Authorization ✅
GET /sessions/:id,POST /sessions/:id/log,GET /sessions/:id/audit-lognow all callresolveStaff()and verifysession.staffId === staffRow.id, returning 403 for non-owners.extendandend.2. Bug — Time-based expiry ✅
checkAndExpireSession()helper auto-expires timed-out sessions in DB.extend,end,GET /sessions/:id, andPOST /sessions/:id/log.3. Test coverage ✅
impersonation.test.tscovering:Verification
QA approved — ready for CTO review and merge.