feat: Staff Impersonation backend + frontend wiring (#75)
* feat: implement Staff Impersonation backend and wire frontend Add server-side impersonation session management with full audit logging, replacing the frontend-only mock. Managers can start time-limited sessions to view the app as a specific client. Backend: - Add impersonation_sessions and impersonation_audit_logs tables (Drizzle schema) with proper FK constraints and status enum - Add Hono API routes: start/get/extend/end session + audit logging - Server-side session expiration, one-active-per-staff enforcement - Staff role validation (manager-only) Frontend: - Add CustomerPortal wrapper with URL-param session init - Add ImpersonationBanner with live countdown timer - Add AuditLogViewer modal for session audit trail - Add "View as Customer" button on Clients page - Auto-log page visits during impersonation Closes #74 Co-Authored-By: Paperclip <noreply@paperclip.ing> * chore: remove unused useNavigate import from Clients.tsx Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: add authorization + expiry checks to impersonation endpoints, add tests Security: Add ownership verification (resolveStaff + staffId check) to GET /sessions/:id, POST /sessions/:id/log, and GET /sessions/:id/audit-log endpoints that were previously unprotected. Bug: Add time-based expiry checks to extend, end, get-session, and log endpoints via checkAndExpireSession() helper. Expired sessions are now auto-marked as expired in the DB and cannot be extended or logged to. Tests: Add 23 tests covering session creation (happy path, auth, conflict), extend (active, expired, non-owner, ended), end (active, expired, non-owner), audit logging (owner, non-owner, expired, ended), and audit-log retrieval (owner, non-owner, not found). Addresses QA review on PR #75 (GRO-66). Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> * fix: resolve @groombook/db source in vitest config Add resolve alias so vitest can resolve @groombook/db from source TypeScript files without requiring a prior build step. Fixes CI test failures when dist/ has not been compiled. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --------- Co-authored-by: Groom Book CEO <ceo@groombook.dev> Co-authored-by: Paperclip <noreply@paperclip.ing> Co-authored-by: Groom Book CTO <cto@groombook.dev> Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: Scrubs McBarkley <scrubs@groombook.app>
This commit was merged in pull request #75.
This commit is contained in:
committed by
GitHub
parent
ea5450651d
commit
70958542f8
@@ -225,6 +225,40 @@ export const reminderLogs = pgTable(
|
||||
(t) => [unique().on(t.appointmentId, t.reminderType)]
|
||||
);
|
||||
|
||||
// ─── Impersonation ──────────────────────────────────────────────────────────
|
||||
|
||||
export const impersonationSessionStatusEnum = pgEnum(
|
||||
"impersonation_session_status",
|
||||
["active", "ended", "expired"]
|
||||
);
|
||||
|
||||
export const impersonationSessions = pgTable("impersonation_sessions", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
staffId: uuid("staff_id")
|
||||
.notNull()
|
||||
.references(() => staff.id, { onDelete: "restrict" }),
|
||||
clientId: uuid("client_id")
|
||||
.notNull()
|
||||
.references(() => clients.id, { onDelete: "restrict" }),
|
||||
reason: text("reason"),
|
||||
status: impersonationSessionStatusEnum("status").notNull().default("active"),
|
||||
startedAt: timestamp("started_at").notNull().defaultNow(),
|
||||
endedAt: timestamp("ended_at"),
|
||||
expiresAt: timestamp("expires_at").notNull(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const impersonationAuditLogs = pgTable("impersonation_audit_logs", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
sessionId: uuid("session_id")
|
||||
.notNull()
|
||||
.references(() => impersonationSessions.id, { onDelete: "cascade" }),
|
||||
action: text("action").notNull(),
|
||||
pageVisited: text("page_visited"),
|
||||
metadata: jsonb("metadata").$type<Record<string, unknown>>(),
|
||||
createdAt: timestamp("created_at").notNull().defaultNow(),
|
||||
});
|
||||
|
||||
export const businessSettings = pgTable("business_settings", {
|
||||
id: uuid("id").primaryKey().defaultRandom(),
|
||||
businessName: text("business_name").notNull().default("GroomBook"),
|
||||
|
||||
Reference in New Issue
Block a user