fix: impersonation, dashboard counter, logo lightbox, submission config, auth logs
All checks were successful
Build and Push Docker Image / build (push) Successful in 9m12s

- Fix impersonation by bypassing useSession().update() loading gate with direct session POST
- Fix dashboard account counter defaulting to latest round with PASSED projects
- Add clickToEnlarge lightbox for project logos on admin detail page
- Remove submission eligibility config (all passed projects must upload)
- Suppress CredentialsSignin auth errors in production (minified name check)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-05 15:40:08 +01:00
parent fd2624f198
commit b7905a82e1
10 changed files with 117 additions and 102 deletions

View File

@@ -3,71 +3,19 @@
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from '@/components/ui/card'
import { Label } from '@/components/ui/label'
import { Switch } from '@/components/ui/switch'
import { Badge } from '@/components/ui/badge'
type SubmissionConfigProps = {
config: Record<string, unknown>
onChange: (config: Record<string, unknown>) => void
}
const STATUSES = [
{ value: 'PENDING', label: 'Pending', color: 'bg-gray-100 text-gray-700' },
{ value: 'IN_PROGRESS', label: 'In Progress', color: 'bg-blue-100 text-blue-700' },
{ value: 'PASSED', label: 'Passed', color: 'bg-emerald-100 text-emerald-700' },
{ value: 'REJECTED', label: 'Rejected', color: 'bg-red-100 text-red-700' },
{ value: 'COMPLETED', label: 'Completed', color: 'bg-purple-100 text-purple-700' },
{ value: 'WITHDRAWN', label: 'Withdrawn', color: 'bg-amber-100 text-amber-700' },
]
export function SubmissionConfig({ config, onChange }: SubmissionConfigProps) {
const update = (key: string, value: unknown) => {
onChange({ ...config, [key]: value })
}
const eligible = (config.eligibleStatuses as string[]) ?? ['PASSED']
const toggleStatus = (status: string) => {
const current = [...eligible]
const idx = current.indexOf(status)
if (idx >= 0) {
current.splice(idx, 1)
} else {
current.push(status)
}
update('eligibleStatuses', current)
}
return (
<div className="space-y-6">
<Card>
<CardHeader>
<CardTitle className="text-base">Submission Eligibility</CardTitle>
<CardDescription>
Which project states from the previous round are eligible to submit documents in this round
</CardDescription>
</CardHeader>
<CardContent className="space-y-4">
<div className="space-y-2">
<Label>Eligible Project Statuses</Label>
<p className="text-xs text-muted-foreground">
Projects with these statuses from the previous round can submit
</p>
<div className="flex flex-wrap gap-2">
{STATUSES.map((s) => (
<Badge
key={s.value}
variant={eligible.includes(s.value) ? 'default' : 'outline'}
className="cursor-pointer select-none"
onClick={() => toggleStatus(s.value)}
>
{s.label}
</Badge>
))}
</div>
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="text-base">Notifications & Locking</CardTitle>

View File

@@ -6,6 +6,7 @@ import type { Route } from 'next'
import { useSession } from 'next-auth/react'
import { useRouter } from 'next/navigation'
import { trpc } from '@/lib/trpc/client'
import { directSessionUpdate } from '@/lib/session-update'
import { Button } from '@/components/ui/button'
import {
DropdownMenu,
@@ -71,7 +72,7 @@ function getRoleHomePath(role: string): string {
export function UserActions({ userId, userEmail, userStatus, userRole, userRoles, currentUserRole }: UserActionsProps) {
const [showDeleteDialog, setShowDeleteDialog] = useState(false)
const [isSending, setIsSending] = useState(false)
const { data: session, update } = useSession()
const { data: session } = useSession()
const router = useRouter()
const utils = trpc.useUtils()
@@ -125,9 +126,12 @@ export function UserActions({ userId, userEmail, userStatus, userRole, userRoles
const handleImpersonate = async () => {
try {
const result = await startImpersonation.mutateAsync({ targetUserId: userId })
await update({ impersonate: userId })
// Full page navigation to ensure the updated JWT cookie is sent
// (router.push can race with cookie propagation)
// Direct POST to session endpoint — bypasses useSession().update()'s loading gate
const ok = await directSessionUpdate({ impersonate: userId })
if (!ok) {
toast.error('Failed to update session for impersonation')
return
}
window.location.href = getRoleHomePath(result.targetRole)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to start impersonation')
@@ -279,7 +283,7 @@ export function UserMobileActions({
currentUserRole,
}: UserMobileActionsProps) {
const [isSending, setIsSending] = useState(false)
const { data: session, update } = useSession()
const { data: session } = useSession()
const router = useRouter()
const utils = trpc.useUtils()
const sendInvitation = trpc.user.sendInvitation.useMutation()
@@ -301,7 +305,11 @@ export function UserMobileActions({
const handleImpersonateMobile = async () => {
try {
const result = await startImpersonation.mutateAsync({ targetUserId: userId })
await update({ impersonate: userId })
const ok = await directSessionUpdate({ impersonate: userId })
if (!ok) {
toast.error('Failed to update session for impersonation')
return
}
window.location.href = getRoleHomePath(result.targetRole)
} catch (error) {
toast.error(error instanceof Error ? error.message : 'Failed to start impersonation')