fix: security hardening — block self-registration, SSE auth, audit logging fixes
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Some checks failed
Build and Push Docker Image / build (push) Has been cancelled
Security fixes: - Block self-registration via magic link (PrismaAdapter createUser throws) - Magic links only sent to existing ACTIVE users (prevents enumeration) - signIn callback rejects non-existent users (defense-in-depth) - Change schema default role from JURY_MEMBER to APPLICANT - Add authentication to live-voting SSE stream endpoint - Fix false FILE_OPENED/FILE_DOWNLOADED audit events on page load (remove purpose from eagerly pre-fetched URL queries) Bug fixes: - Fix impersonation skeleton screen on applicant dashboard - Fix onboarding redirect loop in auth layout Observer dashboard redesign (Steps 1-6): - Clickable round pipeline with selected round highlighting - Round-type-specific dashboard panels (intake, filtering, evaluation, submission, mentoring, live final, deliberation) - Enhanced activity feed with server-side humanization - Previous round comparison section - New backend queries for round-specific analytics Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -1,13 +1,22 @@
|
||||
'use client'
|
||||
|
||||
import { createContext, useContext, useState, useEffect, type ReactNode } from 'react'
|
||||
import { createContext, useContext, useState, useEffect, useMemo, type ReactNode } from 'react'
|
||||
import { trpc } from '@/lib/trpc/client'
|
||||
|
||||
type RoundInfo = {
|
||||
id: string
|
||||
name: string
|
||||
status: string
|
||||
competitionId?: string
|
||||
roundType?: string
|
||||
sortOrder?: number
|
||||
}
|
||||
|
||||
type Program = {
|
||||
id: string
|
||||
name: string | null
|
||||
year?: number
|
||||
rounds?: Array<{ id: string; name: string; status: string; competitionId?: string }>
|
||||
rounds?: RoundInfo[]
|
||||
}
|
||||
|
||||
type EditionContextValue = {
|
||||
@@ -15,6 +24,13 @@ type EditionContextValue = {
|
||||
selectedProgramId: string
|
||||
setSelectedProgramId: (id: string) => void
|
||||
activeRoundId: string
|
||||
/** The user-selected round (defaults to best/active round) */
|
||||
selectedRoundId: string
|
||||
setSelectedRoundId: (id: string) => void
|
||||
/** Derived roundType for the selected round */
|
||||
selectedRoundType: string
|
||||
/** All rounds for the selected program (sorted by sortOrder) */
|
||||
rounds: RoundInfo[]
|
||||
}
|
||||
|
||||
const EditionContext = createContext<EditionContextValue | null>(null)
|
||||
@@ -35,23 +51,37 @@ function findBestRound(rounds: Array<{ id: string; status: string }>): string {
|
||||
|
||||
export function EditionProvider({ children }: { children: ReactNode }) {
|
||||
const [selectedProgramId, setSelectedProgramId] = useState<string>('')
|
||||
const [selectedRoundId, setSelectedRoundId] = useState<string>('')
|
||||
|
||||
const { data: programs } = trpc.program.list.useQuery(
|
||||
{ includeStages: true },
|
||||
{ refetchInterval: 30_000 },
|
||||
)
|
||||
|
||||
useEffect(() => {
|
||||
if (programs && programs.length > 0 && !selectedProgramId) {
|
||||
setSelectedProgramId(programs[0].id)
|
||||
}
|
||||
}, [programs, selectedProgramId])
|
||||
|
||||
const typedPrograms = (programs ?? []) as Program[]
|
||||
const selectedProgram = typedPrograms.find(p => p.id === selectedProgramId)
|
||||
const rounds = (selectedProgram?.rounds ?? []) as Array<{ id: string; status: string }>
|
||||
const rounds = useMemo(
|
||||
() => ((selectedProgram?.rounds ?? []) as RoundInfo[]).slice().sort((a, b) => (a.sortOrder ?? 0) - (b.sortOrder ?? 0)),
|
||||
[selectedProgram?.rounds],
|
||||
)
|
||||
const activeRoundId = findBestRound(rounds)
|
||||
|
||||
// Auto-select first program
|
||||
useEffect(() => {
|
||||
if (typedPrograms.length > 0 && !selectedProgramId) {
|
||||
setSelectedProgramId(typedPrograms[0].id)
|
||||
}
|
||||
}, [typedPrograms, selectedProgramId])
|
||||
|
||||
// Auto-select best round when program changes or rounds load
|
||||
useEffect(() => {
|
||||
if (rounds.length > 0 && (!selectedRoundId || !rounds.some(r => r.id === selectedRoundId))) {
|
||||
setSelectedRoundId(findBestRound(rounds))
|
||||
}
|
||||
}, [rounds, selectedRoundId])
|
||||
|
||||
const selectedRoundType = rounds.find(r => r.id === selectedRoundId)?.roundType ?? ''
|
||||
|
||||
return (
|
||||
<EditionContext.Provider
|
||||
value={{
|
||||
@@ -59,6 +89,10 @@ export function EditionProvider({ children }: { children: ReactNode }) {
|
||||
selectedProgramId,
|
||||
setSelectedProgramId,
|
||||
activeRoundId,
|
||||
selectedRoundId,
|
||||
setSelectedRoundId,
|
||||
selectedRoundType,
|
||||
rounds,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
|
||||
Reference in New Issue
Block a user