-
-
Notifications
You must be signed in to change notification settings - Fork 798
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.
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>
)
}