Skip to content

How To Impersonate Other Users

Johan Eliasson edited this page Mar 13, 2023 · 4 revisions

Impersonating other users is often a critical tool for SaaS applications. Here's how you do it with Blitz.

Screenshot of what it looks like

Add impersonatingFromUserId type to session.

// types.ts
import { DefaultCtx, SessionContext, DefaultPublicData } from "blitz"
import { User } from "db"

declare module "blitz" {
  export interface Ctx extends DefaultCtx {
    session: SessionContext
  }
  export interface PublicData extends DefaultPublicData {
    roles: readonly ["admin" | "customer"]
    userId: User["id"]
    orgId: number
    impersonatingFromUserId?: number
  }
}
// app/auth/mutations/impersonateUser.ts
import { Ctx } from "blitz"
import db from "db"
import * as z from "zod"
import assert from "utils/assert"

export const ImpersonateUserInput = z.object({
  userId: z.number(),
})
export type ImpersonateUserInputType = z.infer<typeof ImpersonateUserInput>

export default async function impersonateUser(input: ImpersonateUserInputType, ctx: Ctx) {
  ctx.session.$authorize("admin")
  const { userId } = ImpersonateUserInput.parse(input)

  const user = await db.user.findFirst({ where: { id: userId } })
  assert(user, "Could not find user id " + userId)

  await ctx.session.$create({
    userId: user.id,
    role: "admin",
    orgId: user.organizationId,
    impersonatingFromUserId: ctx.session.userId,
  })

  return user
}
// app/auth/mutations/stopImpersonating.ts
import { Ctx } from "blitz"
import db from "db"
import assert from "utils/assert"
import { logger } from "utils/logger"

export default async function impersonateUser(_: any, ctx: Ctx) {
  ctx.session.$authorize("admin")

  const userId = ctx.session.publicData.impersonatingFromUserId
  if (!userId) {
    logger.debug("Not impersonating anyone")
    return
  }

  const user = await db.user.findFirst({
    where: { id: userId },
  })
  assert(user, "Could not find user id " + userId)

  await ctx.session.$create({
    userId: user.id,
    role: user.admin ? "admin" : "customer",
    orgId: user.organizationId,
    impersonatingFromUserId: undefined,
  })

  return user
}

Add form similar to this to switch users.

import { useMutation } from "blitz"
import { queryCache } from "react-query"
import impersonateUser, { impersonateUserInput } from "app/auth/mutations/impersonateUser"
import Form from "app/core/components/Form"
import LabeledTextField from "app/core/components/LabeledTextField"

export const ImpersonateUserForm = () => {
  const [impersonateUserMutation] = useMutation(impersonateUser)

  return (
    <Form
      schema={ImpersonateUserInput}
      onSubmit={async (values) => {
        try {
          await impersonateUserMutation(values)
          queryCache.clear()
        } catch (error) {
          return {
            [FORM_ERROR]:
              "Sorry, we had an unexpected error. Please try again. - " + error.toString(),
          }
        }
      }}
    >
      <div className="shadow overflow-hidden sm:rounded-md">
        <div className="px-4 py-5 bg-white sm:p-6">
          <LabeledTextField
            name="userId"
            type="number"
            label="User ID"
            outerProps={{ className: "col-span-6 sm:col-span-3" }}
          />
        </div>
        <div className="px-4 py-3 bg-gray-50 flex justify-end items-center sm:px-6 space-x-3">
          <button disabled={isLoading}>Switch to User</button>
        </div>
      </div>
    </Form>
  )
}

Add this component at the top of your Layout(s).

// app/core/components/ImpersonatingUserNotice.tsx
import { invoke, useSession } from "blitz"
import { queryCache } from "react-query"
import stopImpersonating from "app/auth/mutations/stopImpersonating"

export const ImpersonatingUserNotice = () => {
  const session = useSession()
  if (!session.impersonatingFromUserId) return null

  return (
    <div className="bg-yellow-400 px-2 py-1 text-center font-semibold">
      Currently impersonating user {session.userId}{" "}
      <button
        className="appearance-none bg-transparent text-black uppercase"
        onClick={async () => {
          await invoke(stopImpersonating, {})
          queryCache.clear()
        }}
      >
        Exit
      </button>
    </div>
  )
}