Files
MOPC-Portal/src/components/shared/impersonation-banner.tsx

150 lines
4.7 KiB
TypeScript
Raw Normal View History

'use client'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { useState } from 'react'
import { trpc } from '@/lib/trpc/client'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu'
import { ChevronDown, LogOut, UserCog } from 'lucide-react'
import type { UserRole } from '@prisma/client'
const ROLE_LABELS: Record<string, string> = {
JURY_MEMBER: 'Jury Member',
APPLICANT: 'Applicant',
MENTOR: 'Mentor',
OBSERVER: 'Observer',
AWARD_MASTER: 'Award Master',
PROGRAM_ADMIN: 'Program Admin',
SUPER_ADMIN: 'Super Admin',
}
const ROLE_LANDING: Record<string, string> = {
JURY_MEMBER: '/jury',
APPLICANT: '/applicant',
MENTOR: '/mentor',
OBSERVER: '/observer',
AWARD_MASTER: '/admin',
PROGRAM_ADMIN: '/admin',
SUPER_ADMIN: '/admin',
}
export function ImpersonationBanner() {
const { data: session, update } = useSession()
const router = useRouter()
const [switching, setSwitching] = useState(false)
// Only fetch test users when impersonating (realRole check happens server-side)
const { data: testEnv } = trpc.testEnvironment.status.useQuery(undefined, {
enabled: !!session?.user?.isImpersonating,
staleTime: 60_000,
})
if (!session?.user?.isImpersonating) return null
const currentRole = session.user.role
const currentName = session.user.impersonatedName || session.user.name || 'Unknown'
// Group available test users by role (exclude currently impersonated user)
const availableUsers = testEnv?.active
? testEnv.users.filter((u) => u.id !== session.user.id)
: []
const roleGroups = availableUsers.reduce(
(acc, u) => {
const role = u.role as string
if (!acc[role]) acc[role] = []
acc[role].push(u)
return acc
},
{} as Record<string, typeof availableUsers>
)
async function handleSwitch(userId: string, role: UserRole) {
setSwitching(true)
await update({ impersonateUserId: userId })
router.push((ROLE_LANDING[role] || '/admin') as any)
router.refresh()
setSwitching(false)
}
async function handleStopImpersonation() {
setSwitching(true)
await update({ stopImpersonation: true })
router.push('/admin/settings' as any)
router.refresh()
setSwitching(false)
}
return (
<div className="fixed top-0 left-0 right-0 z-50 bg-amber-500 text-amber-950 shadow-md">
<div className="mx-auto flex items-center justify-between px-4 py-1.5 text-sm font-medium">
<div className="flex items-center gap-2">
<UserCog className="h-4 w-4" />
<span>
Viewing as <strong>{currentName}</strong>{' '}
<span className="rounded bg-amber-600/30 px-1.5 py-0.5 text-xs font-semibold">
{ROLE_LABELS[currentRole] || currentRole}
</span>
</span>
</div>
<div className="flex items-center gap-2">
{/* Quick-switch dropdown */}
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 bg-amber-600/20 text-amber-950 hover:bg-amber-600/40"
disabled={switching}
>
Switch Role
<ChevronDown className="h-3 w-3" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" className="w-56">
{Object.entries(roleGroups).map(([role, users]) => (
<div key={role}>
<DropdownMenuLabel className="text-xs text-muted-foreground">
{ROLE_LABELS[role] || role}
</DropdownMenuLabel>
{users.map((u) => (
<DropdownMenuItem
key={u.id}
onClick={() => handleSwitch(u.id, u.role as UserRole)}
disabled={switching}
>
<span className="truncate">{u.name || u.email}</span>
</DropdownMenuItem>
))}
<DropdownMenuSeparator />
</div>
))}
</DropdownMenuContent>
</DropdownMenu>
{/* Return to admin */}
<Button
variant="ghost"
size="sm"
className="h-7 gap-1 bg-amber-600/20 text-amber-950 hover:bg-amber-600/40"
onClick={handleStopImpersonation}
disabled={switching}
>
<LogOut className="h-3 w-3" />
Return to Admin
</Button>
</div>
</div>
</div>
)
}