You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Support agents frequently need to "see what the user sees" to debug issues (e.g., a farmer's parcel not showing on the map). The better-auth admin plugin provides impersonation, user management, ban/unban, and role management — all critical for a helpdesk team.
Consent request flow: agent clicks "Verzoek toegang" → user receives in-app prompt + email → user approves and picks an expiry date → agent can impersonate until that date
Impersonation guard: impersonateUser() checks for a valid (non-expired, non-revoked) grant before proceeding
Impersonation banner: persistent top-bar during impersonation session (visible on all pages)
User management dashboard (/admin/users): list users, search, set roles, ban/unban, revoke sessions
Client-side changes: add adminClient() plugin to fdm-app/app/lib/auth-client.ts
Integration with ticket view: user info panel showing role, ban status, sessions; request-access / impersonate button (depending on grant status)
Acceptance Criteria
Admin plugin added to fdm-core authentication (server + client)
Running db:generate-authn adds the new columns to user/session tables
Admins can assign helpdeskAgent role to users via /admin/users
Admins can ban/unban users with reason and optional expiry
Agent can request impersonation access from the ticket detail view ("Verzoek toegang")
User receives an in-app notification + email asking them to grant access
User can approve and choose an expiry date (presets: einde van vandaag, morgen, volgende week; or custom date picker; default: einde van vandaag)
User can decline the request
User can revoke a previously granted access at any time from the ticket view
Agent can only impersonate after the user has granted access and the grant has not expired
If no valid grant exists, the "Impersonate" button is disabled with tooltip "Wacht op toestemming van gebruiker"
During impersonation, a persistent amber banner shows "Impersonating: [name]" + "Stop" button + remaining time
Impersonation sessions expire at the earlier of: grant expiry or 1 hour
All events are logged in ticket_activity: access_requested, access_granted, access_declined, access_revoked, impersonation_started, impersonation_ended
Agents can see user context in ticket sidebar: name, email, role, ban status, session count
Only helpdeskAdmin can promote/demote agents and ban users
helpdeskAgent can request impersonation and list users (for context)
Custom access control enforces helpdesk-specific permissions
// fdm-helpdesk schema — new tableexportconstimpersonationGrants=fdmHelpdeskSchema.table("impersonation_grants",{grant_id: text().primaryKey(),ticket_id: text().notNull(),agent_id: text().notNull(),// Agent requesting accessuser_id: text().notNull(),// User granting accessstatus: text().notNull().default("pending"),// 'pending', 'granted', 'declined', 'revoked', 'expired'requested_at: timestamp({withTimezone: true}).notNull().defaultNow(),granted_at: timestamp({withTimezone: true}),granted_until: timestamp({withTimezone: true}),// When access expiresrevoked_at: timestamp({withTimezone: true}),revoked_by: text(),// 'user' or 'agent' or 'system'})
4. Consent Flow
Agent clicks "Verzoek toegang"
│
▼
┌────────────────────────────────────────────────────┐
│ Insert impersonation_grant (status: 'pending') │
│ Log ticket_activity: 'access_requested' │
│ Send in-app notification + email to user │
└────────────────────┬───────────────────────────────┘
│
▼
┌────────────────────────────────────────────────────┐
│ User sees request in ticket view or email: │
│ │
│ "Medewerker Jan wil meekijken in uw account om uw │
│ probleem op te lossen. Tot wanneer wilt u toegang │
│ verlenen?" │
│ │
│ [Vandaag] [Morgen] [1 week] [📅 Kies datum] │
│ [Weigeren] │
└────────────────────┬───────────────────────────────┘
│
┌────────┴────────┐
▼ ▼
User approves User declines
(set granted_at, (set status: 'declined',
granted_until, log activity)
status: 'granted',
log activity)
│
▼
┌────────────────────────────────────────────────────┐
│ Agent sees "Impersonate" button become active │
│ Badge: "Toegang t/m 7 mei" │
│ Agent clicks → impersonateUser() checks grant │
│ → valid? proceed : reject with 403 │
└────────────────────────────────────────────────────┘
5. Impersonation Guard
exportasyncfunctionstartImpersonation(fdm: FdmType,agent_id: string,user_id: string,ticket_id: string,): Promise<void>{// Check for a valid grantconstgrant=awaitfdm.select().from(impersonationGrants).where(and(eq(impersonationGrants.agent_id,agent_id),eq(impersonationGrants.user_id,user_id),eq(impersonationGrants.ticket_id,ticket_id),eq(impersonationGrants.status,"granted"),gt(impersonationGrants.granted_until,newDate()),)).limit(1)if(!grant.length){thrownewError("No valid impersonation grant. User must approve access first.")}// Proceed with better-auth impersonation// The impersonation session duration = min(grant.granted_until - now, 1 hour per session)}
6. UI — Agent Ticket Sidebar (consent states)
── No grant requested ──
┌─── Klantinfo ──────────────────┐
│ Piet de Boer │
│ piet@example.nl │
│ Rol: user · Geblokkeerd: Nee │
│ │
│ [🔑 Verzoek toegang] │
└─────────────────────────────────┘
── Grant pending ──
┌─── Klantinfo ──────────────────┐
│ Piet de Boer │
│ ⏳ Wacht op toestemming... │
│ │
│ [🔑 Verzoek toegang] (grayed) │
└─────────────────────────────────┘
── Grant active ──
┌─── Klantinfo ──────────────────┐
│ Piet de Boer │
│ ✅ Toegang t/m 7 mei │
│ │
│ [👤 Impersonate] [🔒 Blokkeer]│
└─────────────────────────────────┘
── Grant expired / declined ──
┌─── Klantinfo ──────────────────┐
│ Piet de Boer │
│ ❌ Toegang verlopen/geweigerd │
│ │
│ [🔑 Opnieuw verzoeken] │
└─────────────────────────────────┘
7. UI — User Ticket View (consent prompt)
When a grant is pending, the user sees a banner on their ticket:
┌──────────────────────────────────────────────────────────────────┐
│ 🔑 Supportmedewerker Jan wil meekijken in uw account │
│ om uw probleem op te lossen. │
│ │
│ Tot wanneer wilt u toegang verlenen? │
│ [Vandaag] [Morgen] [1 week] [📅 Kies datum] │
│ [Weigeren] │
│ │
│ ℹ️ De medewerker kan uw account bekijken alsof hij u is. │
│ U kunt de toegang op elk moment intrekken. │
└──────────────────────────────────────────────────────────────────┘
After granting:
┌──────────────────────────────────────────────────────────────────┐
│ ✅ U heeft Jan toegang verleend t/m 7 mei. │
│ [Toegang intrekken] │
└──────────────────────────────────────────────────────────────────┘
8. Impersonation Banner Component
// fdm-app/app/components/blocks/support/impersonation-banner.tsx// Persistent amber bar at top of viewport during impersonation// Shows: "⚠️ Impersonating: [User Name] ([email]) — [Stop Impersonating]"// Placed in root layout, rendered conditionally on session.impersonatedBy
Integration tests: User revokes mid-session → verify impersonation ends
Unit tests: Access control permission checks (agent can request, user cannot impersonate)
Unit tests: Grant expiry calculation, status transitions
Manual test: Full consent flow from both agent and user perspectives
Definition of Done
An agent can request access to view a user's account. The user explicitly grants time-limited access from within their ticket. Only then can the agent impersonate. All consent and impersonation events are logged for audit. The user can revoke access at any time.
Context
Support agents frequently need to "see what the user sees" to debug issues (e.g., a farmer's parcel not showing on the map). The better-auth
adminplugin provides impersonation, user management, ban/unban, and role management — all critical for a helpdesk team.Depends on: #599 (MVP — agent roles must exist)
Scope
In Scope
adminplugin integration infdm-core/src/authentication.tsrole,banned,ban_reason,ban_expirescolumns on user table;impersonated_byon session tablecreateAccessControl()for helpdesk-specific permissionsuser(default),helpdeskAgent,helpdeskAdminimpersonation_grantstable: tracks consent (ticket_id, agent_id, user_id, granted_until, revoked_at)impersonateUser()checks for a valid (non-expired, non-revoked) grant before proceedingticket_activity/admin/users): list users, search, set roles, ban/unban, revoke sessionsadminClient()plugin tofdm-app/app/lib/auth-client.tsAcceptance Criteria
fdm-coreauthentication (server + client)db:generate-authnadds the new columns to user/session tableshelpdeskAgentrole to users via/admin/usersticket_activity: access_requested, access_granted, access_declined, access_revoked, impersonation_started, impersonation_endedhelpdeskAdmincan promote/demote agents and ban usershelpdeskAgentcan request impersonation and list users (for context)Technical Implementation
1. Server-Side (
fdm-core/src/authentication.ts)Add to plugins array:
2. Client-Side (
fdm-app/app/lib/auth-client.ts)3. Impersonation Consent Schema
4. Consent Flow
5. Impersonation Guard
6. UI — Agent Ticket Sidebar (consent states)
7. UI — User Ticket View (consent prompt)
When a grant is pending, the user sees a banner on their ticket:
After granting:
8. Impersonation Banner Component
4. User Management Page (
/admin/users)authClient.admin.listUsers({ query: { limit, offset, searchField, searchValue } })authClient.admin.setRole({ userId, role })authClient.admin.banUser({ userId, banReason, banExpiresIn })authClient.admin.unbanUser({ userId })authClient.admin.impersonateUser({ userId })authClient.admin.revokeUserSession({ sessionToken })10. Ticket Detail Integration
In the agent ticket detail sidebar, the "Klantinfo" section adapts based on grant status (see wireframes in section 6 above).
11. Audit Logging
UI Wireframes
See
fdm-helpdesk.md→ "Impersonation Workflow for Helpdesk" and "Admin User Management Dashboard" sections for full wireframes.Testing Requirements
impersonatedBy→ stop → verify revertedDefinition of Done
An agent can request access to view a user's account. The user explicitly grants time-limited access from within their ticket. Only then can the agent impersonate. All consent and impersonation events are logged for audit. The user can revoke access at any time.