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:
2026-03-04 17:55:44 +01:00
parent b1a994a9d6
commit 6c52e519e5
18 changed files with 814 additions and 74 deletions

View File

@@ -1,6 +1,13 @@
import type { NextAuthConfig } from 'next-auth'
import type { UserRole } from '@prisma/client'
type ImpersonationInfo = {
originalId: string
originalRole: UserRole
originalRoles: UserRole[]
originalEmail: string
}
// Extend the built-in session types
declare module 'next-auth' {
interface Session {
@@ -11,6 +18,7 @@ declare module 'next-auth' {
role: UserRole
roles: UserRole[]
mustSetPassword?: boolean
impersonating?: ImpersonationInfo
}
}
@@ -27,6 +35,7 @@ declare module '@auth/core/jwt' {
role: UserRole
roles?: UserRole[]
mustSetPassword?: boolean
impersonating?: ImpersonationInfo
}
}
@@ -61,15 +70,16 @@ export const authConfig: NextAuthConfig = {
return false // Will redirect to signIn page
}
// Check if user needs to set password
// Check if user needs to set password (skip during impersonation)
const mustSetPassword = auth?.user?.mustSetPassword
const isImpersonating = !!(auth?.user as Record<string, unknown>)?.impersonating
const passwordSetupAllowedPaths = [
'/set-password',
'/api/auth',
'/api/trpc',
]
if (mustSetPassword) {
if (mustSetPassword && !isImpersonating) {
// Allow access to password setup related paths
if (passwordSetupAllowedPaths.some((path) => pathname.startsWith(path))) {
return true