fd38f5659f
Let super admins act as a user from the user detail dashboard for
support/debugging ("full with guardrails", audit-only).
Backend: AuthService.impersonate issues a short-lived token for the
target carrying an `act` claim (acting admin); refresh preserves it and
JwtStrategy surfaces `impersonatedBy`. Guardrails: cannot impersonate an
admin or a deactivated account; new ImpersonationGuard blocks sensitive
self-service (change own password/phone) while impersonating. New
AuditLog entity records impersonation start/stop (admin, target, ip,
time); admin endpoints POST users/:id/impersonate + .../impersonation/
stop and GET users/:id/audit.
Frontend: lib/impersonation swaps admin/impersonation tokens in
localStorage; persistent banner with exit; "Login as user" button and an
"Admin access log" tab on the detail page; logout clears impersonation.
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
82 lines
2.9 KiB
TypeScript
82 lines
2.9 KiB
TypeScript
'use client';
|
|
|
|
import api from '@/lib/api';
|
|
|
|
/**
|
|
* Client-side "Login as user" (impersonation) helpers.
|
|
*
|
|
* The admin's own tokens are stashed under `admin*` keys while the active
|
|
* `accessToken`/`refreshToken` are swapped for the impersonation tokens, so the
|
|
* whole app (and the axios interceptor) transparently acts as the target user.
|
|
* Both start and stop are recorded server-side in the audit log.
|
|
*/
|
|
|
|
const IMP_KEY = 'impersonation';
|
|
|
|
export interface ImpersonationInfo {
|
|
targetUserId: string;
|
|
targetName: string;
|
|
}
|
|
|
|
export function getImpersonation(): ImpersonationInfo | null {
|
|
if (typeof window === 'undefined') return null;
|
|
try {
|
|
const raw = localStorage.getItem(IMP_KEY);
|
|
return raw ? (JSON.parse(raw) as ImpersonationInfo) : null;
|
|
} catch {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
export function isImpersonating(): boolean {
|
|
return getImpersonation() !== null;
|
|
}
|
|
|
|
/** Begin impersonating a user. Caller should hard-reload afterwards. */
|
|
export async function startImpersonation(targetUserId: string): Promise<ImpersonationInfo> {
|
|
const { data } = await api.post(`/admin/users/${targetUserId}/impersonate`);
|
|
// Stash the admin session so we can return to it later.
|
|
localStorage.setItem('adminAccessToken', localStorage.getItem('accessToken') ?? '');
|
|
localStorage.setItem('adminRefreshToken', localStorage.getItem('refreshToken') ?? '');
|
|
const info: ImpersonationInfo = {
|
|
targetUserId,
|
|
targetName: `${data.user?.firstName ?? ''} ${data.user?.lastName ?? ''}`.trim(),
|
|
};
|
|
localStorage.setItem(IMP_KEY, JSON.stringify(info));
|
|
// Swap in the impersonation tokens.
|
|
localStorage.setItem('accessToken', data.accessToken);
|
|
localStorage.setItem('refreshToken', data.refreshToken);
|
|
return info;
|
|
}
|
|
|
|
/** End impersonation, restoring the admin session. Caller should hard-reload. */
|
|
export async function stopImpersonation(): Promise<string | null> {
|
|
const info = getImpersonation();
|
|
const adminAccess = localStorage.getItem('adminAccessToken');
|
|
const adminRefresh = localStorage.getItem('adminRefreshToken');
|
|
|
|
// Restore the admin tokens first so the stop call is authorized as the admin.
|
|
if (adminAccess) localStorage.setItem('accessToken', adminAccess);
|
|
if (adminRefresh) localStorage.setItem('refreshToken', adminRefresh);
|
|
localStorage.removeItem('adminAccessToken');
|
|
localStorage.removeItem('adminRefreshToken');
|
|
localStorage.removeItem(IMP_KEY);
|
|
|
|
if (info?.targetUserId) {
|
|
try {
|
|
await api.post(`/admin/users/${info.targetUserId}/impersonation/stop`);
|
|
} catch {
|
|
// Best-effort audit; never block returning to the admin session.
|
|
}
|
|
}
|
|
return info?.targetUserId ?? null;
|
|
}
|
|
|
|
/** Clear any impersonation artefacts (used on full logout). */
|
|
export function clearImpersonation(): void {
|
|
if (typeof window === 'undefined') return;
|
|
localStorage.removeItem('adminAccessToken');
|
|
localStorage.removeItem('adminRefreshToken');
|
|
localStorage.removeItem(IMP_KEY);
|
|
}
|