fix(impersonation): pointer-events + show all roles (§D.4-5)

Banner wrapper now uses pointer-events-none so it doesn't intercept clicks
on the user-menu dropdown sitting underneath; the 'Return to Admin' button
re-enables pointer events on itself only.

Banner also lists every role the impersonated user holds (e.g.
'JURY MEMBER, MENTOR') instead of just the primary role, matching how
multi-role users are surfaced everywhere else.

Plan: docs/superpowers/plans/2026-04-28-pr6-multi-role-and-workspace-previews.md
This commit is contained in:
Matt
2026-04-28 16:05:51 +02:00
parent 432470083c
commit 6475d5c418

View File

@@ -1,15 +1,14 @@
'use client' 'use client'
import { useSession } from 'next-auth/react' import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client' import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button' import { Button } from '@/components/ui/button'
import { ArrowLeft, Loader2 } from 'lucide-react' import { ArrowLeft, Loader2 } from 'lucide-react'
import { toast } from 'sonner' import { toast } from 'sonner'
import type { UserRole } from '@prisma/client'
export function ImpersonationBanner() { export function ImpersonationBanner() {
const { data: session, update } = useSession() const { data: session, update } = useSession()
const router = useRouter()
const endImpersonation = trpc.user.endImpersonation.useMutation() const endImpersonation = trpc.user.endImpersonation.useMutation()
if (!session?.user?.impersonating) return null if (!session?.user?.impersonating) return null
@@ -18,23 +17,29 @@ export function ImpersonationBanner() {
try { try {
await endImpersonation.mutateAsync() await endImpersonation.mutateAsync()
await update({ endImpersonation: true }) await update({ endImpersonation: true })
// Full page navigation to ensure updated JWT cookie is sent
window.location.href = '/admin/members' window.location.href = '/admin/members'
} catch (error) { } catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to end impersonation') toast.error(error instanceof Error ? error.message : 'Failed to end impersonation')
} }
} }
// Show every role the impersonated user holds (multi-role support).
const roles = (session.user.roles as UserRole[] | undefined) ?? [session.user.role as UserRole]
const rolesLabel = roles.map((r) => r.replace(/_/g, ' ')).join(', ')
return ( return (
<div className="fixed top-0 left-0 right-0 z-50 flex items-center justify-center gap-3 bg-red-600 px-4 py-1.5 text-sm text-white shadow-md"> // pointer-events-none on the wrapper so the banner doesn't intercept clicks
// on anything underneath (e.g., the user-menu dropdown). The button inside
// re-enables pointer-events on itself only.
<div className="pointer-events-none fixed top-0 left-0 right-0 z-50 flex items-center justify-center gap-3 bg-red-600 px-4 py-1.5 text-sm text-white shadow-md">
<span> <span>
Impersonating <strong>{session.user.name || session.user.email}</strong>{' '} Impersonating <strong>{session.user.name || session.user.email}</strong>{' '}
({session.user.role.replace('_', ' ')}) ({rolesLabel})
</span> </span>
<Button <Button
size="sm" size="sm"
variant="secondary" variant="secondary"
className="h-6 px-3 text-xs" className="pointer-events-auto h-6 px-3 text-xs"
onClick={handleReturn} onClick={handleReturn}
disabled={endImpersonation.isPending} disabled={endImpersonation.isPending}
> >