150 lines
4.7 KiB
TypeScript
150 lines
4.7 KiB
TypeScript
|
|
'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>
|
||
|
|
)
|
||
|
|
}
|