operations/super-admin-impersonation.md

Super-admin Impersonation — Runbook

Shipped 2026-05-25. Code paths: booking-api/src/auth/auth.service.ts (start/end), booking-api/src/core/superadmin/impersonation.controller.ts (HTTP), booking-api/src/core/superadmin/impersonation-audit.interceptor.ts (audit).

1. Mục đích

Cho phép super-admin truy cập trang quản trị của một tenant dưới danh nghĩa OWNER ảo (hỗ trợ khách hàng, debug data, demo). Mọi mutation đều được audit. Không cần tạo tài khoản OWNER giả hay đổi password tenant.

2. UX flow

Mở session

  1. Super-admin vào /admin/superadmin/tenants.
  2. Click nút LogIn icon (xanh brand) ở cột Actions của tenant row.
  3. Modal hiện ra với:
    • Banner cảnh báo amber ("You are about to act as this salon")
    • Tên + slug tenant
    • Ô textarea Reason (optional) — tự do, 500 chars max
  4. Click Open admin → server POST /superadmin/impersonate/start:
    • Validate caller role = ADMIN
    • Validate tenant tồn tại + isActive
    • Tạo ImpersonationSession row
    • Issue JWT mới (sub=adminId, role=OWNER, tenantId=target, imp claim) → set cookies overwriting admin cookies cũ
  5. Browser hard-reload /admin → AuthContext bootstrap lại → user thấy giao diện OWNER của tenant đó

Trong session

  • Layout admin ((dashboard)/layout.tsx) render <ImpersonationBanner> sticky top với:
    • "Acting as {tenantName} • Logged in by {admin email}"
    • Button Exit
  • Mọi sidebar / page hoạt động đúng như OWNER thật
  • Mỗi POST/PUT/PATCH/DELETE → audit interceptor insert log row (method, path, statusCode, sha256(body))
  • GET requests không log (volume)

Kết thúc session

  1. Click Exit trên banner
  2. POST /superadmin/impersonate/end:
    • Mark endedAt = now() trên session row
    • Revoke imp refresh token (theo hash)
    • Issue admin tokens mới (sub=adminId, role=ADMIN, tenantId=null, no imp)
    • Set cookies overwriting
  3. Browser reload /admin/superadmin/tenants → super-admin context lại

3. Identity & security model

JWT structure

JwtPayload = {
  sub: adminUserId,        // VẪN là admin's user.id → tokenVersion check ăn vào admin row
  tenantId: targetTenantId,
  role: 'OWNER',           // override để guards/RBAC chạy bình thường
  v: admin.tokenVersion,
  imp: { sessionId, adminUserId }  // marker — audit interceptor key off đây
}

Key invariants

  • Admin's tokenVersion = single source of truth. Bump tokenVersion (change password, force logout) → mọi imp token cũng invalid → JwtAuthGuard reject → user phải re-login như admin.
  • Không nest sessions. /start từ chối khi caller JWT đã có imp claim.
  • End chỉ trong session đó. endImpersonation validate session.adminUserId === user.userId mới mark closed.
  • Tenant inactive → refuse. Không impersonate được tenant bị suspend.
  • Audit body = sha256 hash. Không lưu raw để tránh PII / secret leak (GDPR compliance).

Race conditions

  • Admin đổi password trong khi impersonating: tokenVersion bump → next request fail 401. User phải login lại như admin (impersonation session vẫn open trong DB nhưng impossible to use → next idle timeout sẽ stale, không clean tự động — chấp nhận).
  • Tenant bị suspend trong khi đang impersonate: existing imp token vẫn pass JwtAuthGuard (vì check theo admin row). Các business endpoint trên tenant context sẽ start fail dần (BillingGuard, etc.) tùy implementation.

4. Audit page

/admin/superadmin/impersonations (sidebar dưới "Activity"):

  • Bảng: Tenant, Admin email, Reason, Status (Active/Ended), Mutations count, Started, Ended
  • Filter checkbox: "Active sessions only"
  • Click row → drawer hiện audit log entries (method, path, status, body hash)

5. Database schema

model ImpersonationSession {
  id                       String   @id @default(uuid())
  adminUserId              String
  tenantId                 String
  reason                   String?
  originalRefreshTokenHash String?  // reserved, currently unused
  impRefreshTokenHash      String?  // used to revoke precisely on end
  ipAddress                String?
  userAgent                String?
  startedAt                DateTime @default(now())
  endedAt                  DateTime?
  // relations + indexes
}

model ImpersonationAuditLog {
  id         String   @id @default(uuid())
  sessionId  String
  occurredAt DateTime @default(now())
  method     String
  path       String
  statusCode Int
  bodyHash   String?  // sha256, null if empty body
}

Migration: 20260525074345_add_impersonation_session_and_audit.

6. Troubleshooting

Triệu chứng Nguyên nhân Cách fix
Click "Truy cập" → 403 IMPERSONATION_ADMIN_REQUIRED Không phải ADMIN Login bằng admin@booking.no
400 IMPERSONATION_ALREADY_ACTIVE Đã đang impersonate, click thêm lần nữa Click Exit trước hoặc dùng localhost:3020/admin/superadmin/impersonate/end thủ công
Banner không hiện /auth/me chưa trả impersonation block Check JWT cookie có imp claim không (devtools → Application → Cookies → decode accessToken)
Exit click → 404 IMPERSONATION_SESSION_NOT_FOUND Session đã bị xóa hoặc khác admin Force logout /admin/signin, login lại admin

7. Liên quan

  • Auth Architecture — Token versioning + cookies
  • Role Matrix §2.13
  • Code: booking-api/src/auth/auth.service.ts (startImpersonation, endImpersonation, me)
  • Tests: booking-api/src/auth/auth.service.spec.ts, booking-api/src/core/superadmin/*.spec.ts