feat: impersonation system, semi-finalist detail page, tRPC resilience
- Add super-admin impersonation: "Login As" from user list, red banner with "Return to Admin", audit logged start/end, nested impersonation blocked, onboarding gate skipped during impersonation - Fix semi-finalist stats: check latest terminal state (not any PASSED), use passwordHash OR status=ACTIVE for activation check - Add /admin/semi-finalists detail page with search, category/status filters - Add account_reminder_days setting to notifications tab - Add tRPC resilience: retry on 503/HTML responses, custom fetch detects nginx error pages, exponential backoff (2s/4s/8s) - Reduce dashboard polling intervals (60s stats, 30s activity, 120s semi) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
51
src/components/shared/impersonation-banner.tsx
Normal file
51
src/components/shared/impersonation-banner.tsx
Normal file
@@ -0,0 +1,51 @@
|
||||
'use client'
|
||||
|
||||
import { useSession } from 'next-auth/react'
|
||||
import { useRouter } from 'next/navigation'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
import { Button } from '@/components/ui/button'
|
||||
import { ArrowLeft, Loader2 } from 'lucide-react'
|
||||
import { toast } from 'sonner'
|
||||
|
||||
export function ImpersonationBanner() {
|
||||
const { data: session, update } = useSession()
|
||||
const router = useRouter()
|
||||
const endImpersonation = trpc.user.endImpersonation.useMutation()
|
||||
|
||||
if (!session?.user?.impersonating) return null
|
||||
|
||||
const handleReturn = async () => {
|
||||
try {
|
||||
await endImpersonation.mutateAsync()
|
||||
await update({ endImpersonation: true })
|
||||
toast.success('Returned to admin account')
|
||||
router.push('/admin/members')
|
||||
router.refresh()
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : 'Failed to end impersonation')
|
||||
}
|
||||
}
|
||||
|
||||
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">
|
||||
<span>
|
||||
Impersonating <strong>{session.user.name || session.user.email}</strong>{' '}
|
||||
({session.user.role.replace('_', ' ')})
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
variant="secondary"
|
||||
className="h-6 px-3 text-xs"
|
||||
onClick={handleReturn}
|
||||
disabled={endImpersonation.isPending}
|
||||
>
|
||||
{endImpersonation.isPending ? (
|
||||
<Loader2 className="mr-1 h-3 w-3 animate-spin" />
|
||||
) : (
|
||||
<ArrowLeft className="mr-1 h-3 w-3" />
|
||||
)}
|
||||
Return to Admin
|
||||
</Button>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user