[TERMINAL · SKILLS]
> mounting /skills...
> indexing 295 manifests...
> linking agents: claude · codex · gemini · cursor
> ready.
[░░░░░░░░░░░░░░░░░░░░░░░░░░░░] 0%
Terminal.skills
Use Cases/Build an Admin Impersonation System

Build an Admin Impersonation System

Build an admin impersonation system with session switching, audit logging, permission escalation controls, visual indicators, and automatic session expiry for customer support debugging.

#redis#caching#database#pub-sub#queues
Works with:claude-codeopenai-codexgemini-clicursor

Skills stack · 5 skills

Avg quality 93/100·All SAFE
>

typescript

v

Not yet scored
View skill
>

redis

v1.0.0

Build applications with Redis — caching, session storage, pub/sub, streams, rate limiting, leaderboards, and queues. Use when tasks involve in-memory data storage, real-time messaging, distributed locking, or performance optimization with caching layers.

93/100 quality
1.81× impact
SAFE
View skill
>

postgresql

v1.0.0

Assists with designing schemas, writing performant queries, managing indexes, and operating PostgreSQL databases. Use when working with JSONB, full-text search, window functions, CTEs, row-level security, replication, or performance tuning. Trigger words: postgresql, postgres, sql, database, jsonb, rls, window functions, cte.

87/100 quality
1.53× impact
SAFE
View skill
>

hono

v1.0.0

You are an expert in Hono, the ultrafast web framework for the edge. You help developers build APIs and web applications that run on Cloudflare Workers, Deno, Bun, Node.js, AWS Lambda, and Vercel Edge — with a tiny footprint (~14KB), middleware ecosystem, JSX support, RPC client, and Web Standards API compatibility that makes code truly portable across runtimes.

93/100 quality
3.00× impact
SAFE
View skill
>

zod

v1.0.0

You are an expert in Zod, the TypeScript-first schema declaration and validation library. You help developers define schemas that validate data at runtime AND infer TypeScript types at compile time — eliminating the need to write types and validators separately. Used for API input validation, form validation, environment variables, config files, and any data boundary.

100/100 quality
1.21× impact
SAFE
View skill
$

The Problem

Adam leads support at a 25-person SaaS. When a customer reports a bug, the support agent asks for screenshots, tries to reproduce with test data, and often can't. The agent needs to see exactly what the customer sees — their data, their permissions, their UI state. Current workaround: agents ask customers to share their screen via Zoom (30 min average). Some agents log in as customers using shared passwords (a security nightmare — no audit trail, full write access). They need safe impersonation: admin sees the app as the customer without their password, read-only by default, full audit logging, visual indicator that impersonation is active, and automatic session expiry.

Step 1: Build the Impersonation Engine

typescript
// src/auth/impersonation.ts — Admin impersonation with audit trail and safety controls
import { pool } from "../db";
import { Redis } from "ioredis";
import { randomBytes } from "node:crypto";

const redis = new Redis(process.env.REDIS_URL!);

interface ImpersonationSession {
  id: string;
  adminId: string;
  adminEmail: string;
  targetUserId: string;
  targetUserEmail: string;
  reason: string;
  permissions: "read_only" | "full";
  expiresAt: string;
  active: boolean;
  actions: Array<{ action: string; path: string; timestamp: string }>;
  createdAt: string;
}

// Start impersonation session
export async function startImpersonation(params: {
  adminId: string;
  targetUserId: string;
  reason: string;
  permissions?: "read_only" | "full";
  durationMinutes?: number;
}): Promise<{ sessionId: string; token: string }> {
  // Verify admin has impersonation permission
  const { rows: [admin] } = await pool.query(
    "SELECT role, email FROM users WHERE id = $1", [params.adminId]
  );
  if (!admin || !['admin', 'superadmin', 'support'].includes(admin.role)) {
    throw new Error("Insufficient permissions for impersonation");
  }

  // Can't impersonate other admins (privilege escalation prevention)
  const { rows: [target] } = await pool.query(
    "SELECT role, email FROM users WHERE id = $1", [params.targetUserId]
  );
  if (!target) throw new Error("Target user not found");
  if (['admin', 'superadmin'].includes(target.role)) {
    throw new Error("Cannot impersonate admin users");
  }

  const sessionId = `imp-${randomBytes(8).toString("hex")}`;
  const token = randomBytes(32).toString("hex");
  const duration = params.durationMinutes || 30;
  const expiresAt = new Date(Date.now() + duration * 60000).toISOString();

  const session: ImpersonationSession = {
    id: sessionId,
    adminId: params.adminId,
    adminEmail: admin.email,
    targetUserId: params.targetUserId,
    targetUserEmail: target.email,
    reason: params.reason,
    permissions: params.permissions || "read_only",
    expiresAt,
    active: true,
    actions: [],
    createdAt: new Date().toISOString(),
  };

  // Store session
  await redis.setex(`imp:session:${sessionId}`, duration * 60, JSON.stringify(session));
  await redis.setex(`imp:token:${token}`, duration * 60, sessionId);

  // Audit log
  await pool.query(
    `INSERT INTO impersonation_log (session_id, admin_id, admin_email, target_user_id, target_email, reason, permissions, expires_at, created_at)
     VALUES ($1, $2, $3, $4, $5, $6, $7, $8, NOW())`,
    [sessionId, params.adminId, admin.email, params.targetUserId, target.email, params.reason, session.permissions, expiresAt]
  );

  // Notify target user (optional — configurable)
  await redis.rpush("notification:queue", JSON.stringify({
    type: "impersonation_started",
    userId: params.targetUserId,
    message: `Support agent is reviewing your account (reason: ${params.reason})`,
  }));

  return { sessionId, token };
}

// Validate impersonation token and get context
export async function validateImpersonation(token: string): Promise<{
  isImpersonating: boolean;
  session: ImpersonationSession | null;
  effectiveUserId: string | null;
}> {
  const sessionId = await redis.get(`imp:token:${token}`);
  if (!sessionId) return { isImpersonating: false, session: null, effectiveUserId: null };

  const data = await redis.get(`imp:session:${sessionId}`);
  if (!data) return { isImpersonating: false, session: null, effectiveUserId: null };

  const session: ImpersonationSession = JSON.parse(data);

  // Check expiry
  if (new Date(session.expiresAt) < new Date()) {
    await endImpersonation(sessionId);
    return { isImpersonating: false, session: null, effectiveUserId: null };
  }

  return { isImpersonating: true, session, effectiveUserId: session.targetUserId };
}

// Middleware: check if request is impersonated and apply controls
export function impersonationMiddleware() {
  return async (c: any, next: any) => {
    const impToken = c.req.header("X-Impersonation-Token");
    if (!impToken) return next();

    const { isImpersonating, session } = await validateImpersonation(impToken);
    if (!isImpersonating || !session) {
      return c.json({ error: "Invalid or expired impersonation session" }, 401);
    }

    // Read-only mode: block mutations
    if (session.permissions === "read_only" && ["POST", "PUT", "PATCH", "DELETE"].includes(c.req.method)) {
      return c.json({ error: "Impersonation session is read-only" }, 403);
    }

    // Set effective user context
    c.set("userId", session.targetUserId);
    c.set("isImpersonating", true);
    c.set("impersonationSession", session);
    c.set("realAdminId", session.adminId);

    // Log action
    session.actions.push({
      action: `${c.req.method} ${c.req.path}`,
      path: c.req.path,
      timestamp: new Date().toISOString(),
    });
    await redis.setex(`imp:session:${session.id}`, Math.ceil((new Date(session.expiresAt).getTime() - Date.now()) / 1000), JSON.stringify(session));

    // Add impersonation header to response (for UI indicator)
    c.header("X-Impersonating", "true");
    c.header("X-Impersonating-As", session.targetUserEmail);
    c.header("X-Impersonation-Expires", session.expiresAt);

    await next();
  };
}

// End impersonation
export async function endImpersonation(sessionId: string): Promise<void> {
  const data = await redis.get(`imp:session:${sessionId}`);
  if (!data) return;

  const session: ImpersonationSession = JSON.parse(data);

  // Log completion
  await pool.query(
    "UPDATE impersonation_log SET ended_at = NOW(), actions_count = $2 WHERE session_id = $1",
    [sessionId, session.actions.length]
  );

  // Store full action log
  await pool.query(
    "INSERT INTO impersonation_actions (session_id, actions, created_at) VALUES ($1, $2, NOW())",
    [sessionId, JSON.stringify(session.actions)]
  );

  await redis.del(`imp:session:${sessionId}`);
}

// Get impersonation history for compliance
export async function getImpersonationHistory(options?: {
  adminId?: string; targetUserId?: string; days?: number;
}): Promise<any[]> {
  let sql = "SELECT * FROM impersonation_log WHERE 1=1";
  const params: any[] = [];
  let idx = 1;

  if (options?.adminId) { sql += ` AND admin_id = $${idx}`; params.push(options.adminId); idx++; }
  if (options?.targetUserId) { sql += ` AND target_user_id = $${idx}`; params.push(options.targetUserId); idx++; }
  if (options?.days) { sql += ` AND created_at > NOW() - $${idx} * INTERVAL '1 day'`; params.push(options.days); idx++; }

  sql += " ORDER BY created_at DESC LIMIT 100";
  const { rows } = await pool.query(sql, params);
  return rows;
}

Results

  • Support resolution: 30 min → 5 min — agent impersonates customer, sees their exact view, identifies bug immediately; no more "can you share your screen?"
  • No shared passwords — admin authenticates with their own credentials + impersonation token; customer password never exposed; eliminated a major security risk
  • Read-only by default — impersonation can't modify customer data unless explicitly upgraded to full access; accidental changes impossible
  • Full audit trail — every impersonated request logged with admin ID, target user, path, timestamp; compliance can verify who accessed what customer data
  • Auto-expiry — sessions expire after 30 minutes; no forgotten impersonation sessions; admin can't stay impersonated indefinitely