Blitz is in beta! 🎉 1.0 expected in May or June
Back to Documentation Menu

How To Impersonate Other Users

Impersonation is often a critical tool for SaaS applications as it allows admins to view the application from the perspective of a specific user without knowing their credentials.

Here's an example of what your final result should look like: Screenshot of what it looks like

First, add a impersonatingFromUserId type to your 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 {
    role: "admin" | "customer"
    userId: User["id"]
    orgId: User["orgId"]
    impersonatingFromUserId?: number
  }
}

Create the following two mutations which will give you the ability to start the impersonation as well as stopping it.

// app/auth/mutations/impersonateUser.ts
import { resolver } from "blitz"
import db from "db"
import * as z from "zod"

export const ImpersonateUserInput = z.object({
  userId: z.number(),
})

export default resolver.pipe(
  resolver.zod(ImpersonateUserInput),
  resolver.authorize(),
  async ({ userId }, ctx) => {
    const user = await db.user.findFirst({ where: { id: userId } })
    if (!user) throw new Error("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 { resolver } from "blitz"
import db from "db"

export default resolver.pipe(resolver.authorize(), async (_, ctx) => {
  const userId = ctx.session.$publicData.impersonatingFromUserId
  if (!userId) {
    console.log("Already not impersonating anyone")
    return
  }

  const user = await db.user.findFirst({
    where: { id: userId },
  })
  if (!user) throw new Error("Could not find user id " + userId)

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

  return user
})

Add a form similar to this in order to switch users.

import { useMutation, queryClient } from "blitz"
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}
      submitText="Switch to User"
      onSubmit={async (values) => {
        try {
          await impersonateUserMutation(values)
          queryClient.clear()
        } catch (error) {
          return {
            [FORM_ERROR]:
              "Sorry, we had an unexpected error. Please try again. - " +
              error.toString(),
          }
        }
      }}
    >
      <LabeledTextField name="userId" type="number" label="User ID" />
    </Form>
  )
}

Lastly, add the following component at the top of your Layout(s).

// app/core/components/ImpersonatingUserNotice.tsx
import { invoke, useSession, queryClient } from "blitz"
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, {})
          queryClient.clear()
        }}
      >
        Exit
      </button>
    </div>
  )
}

You're done! You are now able to impersonate a user and stop again.


Idea for improving this page? Edit it on GitHub.