-
-
Notifications
You must be signed in to change notification settings - Fork 807
How To Impersonate Other Users
Brandon Bayer edited this page Jan 22, 2021
·
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("Already 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.
<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 { Button } from "app/core/components/Button"
import stopImpersonating from "app/auth/mutations/stopImpersonating"
import { queryCache } from "react-query"
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
color="transparent"
textColor="black"
onClick={async () => {
await invoke(stopImpersonating, {})
queryCache.clear()
}}
>
EXIT
</Button>
</div>
)
}