# Admin User Role & Dealer Impersonation

**Date:** 2025-12-03
**Status:** Approved
**Priority:** Beta-critical

## Overview

Admin users can log in via the same Google/Apple OAuth flow as dealers. Users with `isAdmin: true` on their User record gain access to an admin dashboard where they can view dealer statistics, search for dealers, and impersonate any dealer account for support purposes.

## Goals

- Allow internal staff to log in as any dealer for support, migration, and troubleshooting
- Provide admin dashboard with dealer overview and search
- Maintain audit trail of impersonation events
- Enable admins to test their own dealer accounts without separate Google accounts

## Design Decisions

| Decision                | Choice                                                             | Rationale                                                       |
| ----------------------- | ------------------------------------------------------------------ | --------------------------------------------------------------- |
| Admin authentication    | Same OAuth + `isAdmin` flag                                        | Leverages existing auth, enables future in-app admin management |
| Email constraint        | `@aimclear.com` only                                               | Safety guardrail for admin access                               |
| Impersonation mechanism | Dual-token (backup cookie)                                         | Clean separation, easy restore, industry pattern                |
| Permission level        | Full dealer access                                                 | Simpler, better for support/debugging                           |
| Audit logging           | Impersonation events only                                          | Good hygiene without over-engineering                           |
| Impersonation timeout   | 4 hours                                                            | Covers typical support sessions                                 |
| Admin route protection  | Signed-in non-admin → `/dashboard`, not signed in → `/auth/signin` |

## Data Model Changes

```prisma
model User {
  // ... existing fields ...
  isAdmin  Boolean @default(false)

  // Relations for audit logging
  adminAuditLogs    AdminAuditLog[] @relation("AdminActions")
  dealerAuditLogs   AdminAuditLog[] @relation("DealerAuditLogs")
}

model AdminAuditLog {
  id            String   @id @default(cuid())
  adminUserId   String
  dealerUserId  String
  action        String   // "impersonation_start" | "impersonation_end"
  ipAddress     String?
  userAgent     String?
  createdAt     DateTime @default(now())

  adminUser     User     @relation("AdminActions", fields: [adminUserId], references: [id])
  dealerUser    User     @relation("DealerAuditLogs", fields: [dealerUserId], references: [id])

  @@index([adminUserId])
  @@index([dealerUserId])
  @@index([createdAt])
}
```

**Constraint:** Only `@aimclear.com` email addresses can have `isAdmin: true`. Enforced at the application level.

## Authentication & Authorization

### Session Enhancement

Extend JWT and session to include admin status:

```typescript
// In JWT callback
token.isAdmin = dbUser.isAdmin ?? false;

// In session callback
session.user.isAdmin = token.isAdmin;

// types/next-auth.d.ts
declare module 'next-auth' {
  interface Session extends DefaultSession {
    user?: DefaultSession['user'] & {
      id?: string | null;
      isAdmin?: boolean;
      impersonation?: {
        adminUserId: string;
        startedAt: number;
        expiresAt: number;
      };
    };
  }
}
```

### Route Protection

| Route Pattern  | Not Signed In    | Signed In (Non-Admin) | Signed In (Admin) |
| -------------- | ---------------- | --------------------- | ----------------- |
| `/admin/*`     | → `/auth/signin` | → `/dashboard`        | Access granted    |
| `/dashboard/*` | → `/auth/signin` | Access granted        | Access granted    |

## Impersonation Mechanism

### Dual-Token Approach

1. **Backup admin session** - Store current admin JWT in separate `HttpOnly` cookie (`admin-backup-session`), signed with HMAC-SHA256
2. **Issue dealer session** - Create new JWT with dealer's user ID plus impersonation metadata
3. **Switch back** - Restore backup cookie to main session, delete backup

### Impersonation JWT Claims

```typescript
{
  sub: "dealer-user-id",
  isAdmin: false,
  impersonation: {
    adminUserId: "admin-user-id",
    startedAt: 1701619200,
    expiresAt: 1701633600  // 4 hours later
  }
}
```

### Cookie Configuration

| Cookie                    | Purpose                           | Max Age                                     |
| ------------------------- | --------------------------------- | ------------------------------------------- |
| `next-auth.session-token` | Active session                    | 4 hours (impersonation) or 30 days (normal) |
| `admin-backup-session`    | Admin's original session (signed) | 4 hours                                     |

Both cookies: `HttpOnly`, `Secure`, `SameSite=Lax`

### Security Constraints

- Cannot impersonate while already impersonating (middleware check)
- Backup cookie signed with `NEXTAUTH_SECRET`
- Auto-restore admin session when impersonation expires

## Admin UI

### Route Structure

```
/admin                    → Admin dashboard (stats + search)
/admin/dealers            → Full dealer list with pagination
/api/admin/impersonate    → POST: start impersonation
/api/admin/end-session    → POST: end impersonation
/api/admin/dealers        → GET: dealer search/list
/api/admin/stats          → GET: dashboard statistics
```

### Dashboard Components

1. **Stats Cards** - Total dealers, by subscription tier, by status, recently created
2. **Quick Action** - "View my dealer account" button (if admin has associated dealer)
3. **Dealer Search** - Fuzzy search by name, email, subdomain, dealer number
4. **Dealer Table** - Paginated list with Name, Email, Subdomain, Tier, Status, Actions

### Impersonation Header

When impersonating, the `/dashboard` header shows:

- Different background color (amber/orange tint)
- Admin badge
- "Return to Admin" button
- Currently viewed dealer info

Note: "Impersonation" terminology is internal only. User-facing text says "Return to Admin".

## Audit Logging

### Events Logged

| Event                 | When                                      | Data                                                |
| --------------------- | ----------------------------------------- | --------------------------------------------------- |
| `impersonation_start` | Admin clicks "Impersonate"                | adminUserId, dealerUserId, IP, userAgent, timestamp |
| `impersonation_end`   | Admin clicks "Return to Admin" or timeout | Same                                                |

### Not Logged (YAGNI)

- Individual actions during impersonation
- Page views during impersonation
- Failed impersonation attempts

## Implementation Files

### New Files

| File                                   | Purpose                           |
| -------------------------------------- | --------------------------------- |
| `app/admin/page.tsx`                   | Admin dashboard                   |
| `app/admin/layout.tsx`                 | Admin layout with auth protection |
| `app/api/admin/impersonate/route.ts`   | Start impersonation               |
| `app/api/admin/end-session/route.ts`   | End impersonation                 |
| `app/api/admin/dealers/route.ts`       | Dealer search/list                |
| `app/api/admin/stats/route.ts`         | Dashboard stats                   |
| `components/admin/DealerSearch.tsx`    | Fuzzy search component            |
| `components/admin/DealerTable.tsx`     | Paginated dealer list             |
| `components/admin/StatsCards.tsx`      | Dashboard stat cards              |
| `components/dashboard/AdminHeader.tsx` | Header variant for impersonation  |

### Modified Files

| File                              | Changes                                      |
| --------------------------------- | -------------------------------------------- |
| `prisma/schema.prisma`            | Add `isAdmin`, `AdminAuditLog`               |
| `lib/auth.ts`                     | Add `isAdmin` to JWT/session callbacks       |
| `types/next-auth.d.ts`            | Extend session types                         |
| `middleware.ts`                   | Admin route protection, impersonation checks |
| `components/dashboard/Header.tsx` | Detect impersonation, show admin variant     |

## Post-MVP Considerations

- Mobile/tablet responsive admin table (currently desktop-only)
- Action-level audit logging if needed
- In-app admin management UI (grant/revoke `isAdmin`)
- Failed impersonation attempt logging
