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
- Super-admin vào
/admin/superadmin/tenants. - Click nút LogIn icon (xanh brand) ở cột Actions của tenant row.
- 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
- Click Open admin → server
POST /superadmin/impersonate/start:- Validate caller role = ADMIN
- Validate tenant tồn tại + isActive
- Tạo
ImpersonationSessionrow - Issue JWT mới (sub=adminId, role=OWNER, tenantId=target, imp claim) → set cookies overwriting admin cookies cũ
- 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
- Click Exit trên banner
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
- Mark
- 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.
/starttừ chối khi caller JWT đã cóimpclaim. - End chỉ trong session đó.
endImpersonationvalidatesession.adminUserId === user.userIdmớ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