Loading...
Loading...
> View the application as any user for debugging and customer support.
User impersonation allows admins to temporarily view the application as any user without knowing their password. Essential for debugging user-reported issues, verifying permissions, and providing customer support. All impersonation sessions are logged to the audit trail for compliance.
API to begin impersonating a user
1// src/app/api/admin/users/impersonate/route.ts2import { NextResponse } from "next/server";3import { auth } from "@/lib/auth";4import { prisma } from "@/lib/db";5import { createAuditLog } from "@/lib/audit-log";6import { cookies } from "next/headers";78const IMPERSONATION_COOKIE = "fabrk_impersonation";910export async function POST(req: Request) {11 const session = await auth();1213 // Only admins can impersonate14 if (!session?.user || session.user.role !== "ADMIN") {15 return NextResponse.json({ error: "Unauthorized" }, { status: 401 });16 }1718 const { targetUserId, reason } = await req.json();1920 // Get target user21 const targetUser = await prisma.user.findUnique({22 where: { id: targetUserId },23 });2425 if (!targetUser) {26 return NextResponse.json({ error: "User not found" }, { status: 404 });27 }2829 // Prevent impersonating other admins30 if (targetUser.role === "ADMIN") {31 return NextResponse.json(32 { error: "Cannot impersonate admin users" },33 { status: 403 }34 );35 }3637 // Log the impersonation38 await createAuditLog({39 action: "admin.user_impersonated",40 category: "admin",41 severity: "warning",42 userId: session.user.id,43 targetType: "user",44 targetId: targetUserId,45 metadata: {46 reason,47 targetEmail: targetUser.email,48 adminEmail: session.user.email,49 },50 });5152 // Set impersonation cookie53 const cookieStore = await cookies();54 cookieStore.set(IMPERSONATION_COOKIE, JSON.stringify({55 originalUserId: session.user.id,56 targetUserId,57 startedAt: new Date().toISOString(),58 }), {59 httpOnly: true,60 secure: process.env.NODE_ENV === "production",61 sameSite: "lax",62 path: "/",63 maxAge: 60 * 60, // 1 hour max64 });6566 return NextResponse.json({67 success: true,68 targetUser: {69 id: targetUser.id,70 name: targetUser.name,71 email: targetUser.email,72 },73 });74}
API to stop impersonating and return to admin session
1// DELETE handler in the same route2export async function DELETE() {3 const cookieStore = await cookies();4 const impersonationData = cookieStore.get(IMPERSONATION_COOKIE);56 if (!impersonationData) {7 return NextResponse.json(8 { error: "No active impersonation" },9 { status: 400 }10 );11 }1213 // Clear the cookie14 cookieStore.delete(IMPERSONATION_COOKIE);1516 // Log the exit17 const data = JSON.parse(impersonationData.value);18 await createAuditLog({19 action: "admin.impersonation_ended",20 category: "admin",21 severity: "info",22 userId: data.originalUserId,23 targetType: "user",24 targetId: data.targetUserId,25 metadata: {26 duration: Date.now() - new Date(data.startedAt).getTime(),27 },28 });2930 return NextResponse.json({ success: true });31}
Visual indicator shown when impersonating
1// src/components/admin/impersonation-banner.tsx2"use client";34import { useState, useEffect } from "react";5import { Button } from "@/components/ui/button";6import { AlertTriangle } from "lucide-react";78export function ImpersonationBanner() {9 const [impersonating, setImpersonating] = useState<{10 targetName: string;11 targetEmail: string;12 } | null>(null);1314 useEffect(() => {15 // Check impersonation status on mount16 fetch("/api/admin/users/impersonate")17 .then((res) => res.json())18 .then((data) => {19 if (data.isImpersonating) {20 setImpersonating({21 targetName: data.targetUser.name,22 targetEmail: data.targetUser.email,23 });24 }25 });26 }, []);2728 if (!impersonating) return null;2930 const handleExit = async () => {31 await fetch("/api/admin/users/impersonate", { method: "DELETE" });32 window.location.href = "/admin/users";33 };3435 return (36 <div className="fixed top-0 left-0 right-0 z-50 bg-destructive text-destructive-foreground">37 <div className="container flex items-center justify-between py-2">38 <div className="flex items-center gap-2 font-mono text-sm">39 <AlertTriangle className="h-4 w-4" />40 <span>41 IMPERSONATING: {impersonating.targetName} ({impersonating.targetEmail})42 </span>43 </div>44 <Button45 variant="outline"46 size="sm"47 onClick={handleExit}48 className={cn('font-mono text-xs', mode.radius)}49 >50 > EXIT IMPERSONATION51 </Button>52 </div>53 </div>54 );55}
Button to start impersonation from user management
1// src/components/admin/impersonate-button.tsx2"use client";34import { useState } from "react";5import { Button } from "@/components/ui/button";6import {7 Dialog,8 DialogContent,9 DialogDescription,10 DialogFooter,11 DialogHeader,12 DialogTitle,13 DialogTrigger,14} from "@/components/ui/dialog";15import { Textarea } from "@/components/ui/textarea";16import { UserCog } from "lucide-react";1718interface ImpersonateButtonProps {19 userId: string;20 userName: string;21}2223export function ImpersonateButton({ userId, userName }: ImpersonateButtonProps) {24 const [open, setOpen] = useState(false);25 const [reason, setReason] = useState("");26 const [loading, setLoading] = useState(false);2728 const handleImpersonate = async () => {29 setLoading(true);3031 const response = await fetch("/api/admin/users/impersonate", {32 method: "POST",33 headers: { "Content-Type": "application/json" },34 body: JSON.stringify({ targetUserId: userId, reason }),35 });3637 if (response.ok) {38 window.location.href = "/dashboard";39 } else {40 const error = await response.json();41 alert(error.error);42 }4344 setLoading(false);45 };4647 return (48 <Dialog open={open} onOpenChange={setOpen}>49 <DialogTrigger asChild>50 <Button variant="ghost" size="sm">51 <UserCog className="h-4 w-4" />52 </Button>53 </DialogTrigger>54 <DialogContent>55 <DialogHeader>56 <DialogTitle className="font-mono">IMPERSONATE USER</DialogTitle>57 <DialogDescription>58 You are about to view the application as {userName}. This action will be logged.59 </DialogDescription>60 </DialogHeader>61 <Textarea62 placeholder="Reason for impersonation (required)..."63 value={reason}64 onChange={(e) => setReason(e.target.value)}65 />66 <DialogFooter>67 <Button variant="outline" onClick={() => setOpen(false)}>68 > CANCEL69 </Button>70 <Button71 onClick={handleImpersonate}72 disabled={!reason.trim() || loading}73 >74 {loading ? "Starting..." : "> START IMPERSONATION"}75 </Button>76 </DialogFooter>77 </DialogContent>78 </Dialog>79 );80}
Handle with Care
All impersonation activity is logged to the audit trail:
admin.user_impersonatedImpersonation startedadmin.impersonation_endedAdmin returned to their sessionuser.action_during_impersonationActions taken while impersonatingAdd the impersonation banner to your dashboard layout:
// src/app/(dashboard)/layout.tsx
import { ImpersonationBanner } from "@/components/admin/impersonation-banner";
export default function DashboardLayout({ children }) {
return (
<>
<ImpersonationBanner />
{children}
</>
);
}