diff --git a/app/api/interview/[id]/chaos-timeout/route.ts b/app/api/interview/[id]/chaos-timeout/route.ts index 0363f12..cbb4973 100644 --- a/app/api/interview/[id]/chaos-timeout/route.ts +++ b/app/api/interview/[id]/chaos-timeout/route.ts @@ -1,6 +1,6 @@ import { NextRequest, NextResponse } from 'next/server'; import dbConnect from '@/src/lib/db/mongoose'; -import InterviewSession from '@/src/lib/db/models/InterviewSession'; +import InterviewSession, { IConstraintChange } from '@/src/lib/db/models/InterviewSession'; export async function POST( req: NextRequest, @@ -19,7 +19,7 @@ export async function POST( // 2. Locate Constraint const constraintChanges = session.constraintChanges || []; - const constraintIndex = constraintChanges.findIndex((c: { id: string }) => c.id === constraintId); + const constraintIndex = constraintChanges.findIndex((c: IConstraintChange) => c.id === constraintId); if (constraintIndex === -1) { return NextResponse.json({ error: 'Constraint not found' }, { status: 404 }); } @@ -62,4 +62,4 @@ export async function POST( console.error('Chaos Timeout Error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/app/api/interview/[id]/final-validation/route.ts b/app/api/interview/[id]/final-validation/route.ts index 2561940..196b73a 100644 --- a/app/api/interview/[id]/final-validation/route.ts +++ b/app/api/interview/[id]/final-validation/route.ts @@ -1,16 +1,38 @@ import { NextRequest, NextResponse } from 'next/server'; -import dbConnect from '@/src/lib/db/mongoose'; +import dbConnect, { isValidObjectId } from '@/src/lib/db/mongoose'; import InterviewSession from '@/src/lib/db/models/InterviewSession'; +import User from '@/src/lib/db/models/User'; +import { getAuthenticatedUser } from '@/src/lib/firebase/firebaseAdmin'; export async function POST( req: NextRequest, context: { params: Promise<{ id: string }> } ) { try { - await dbConnect(); const { id } = await context.params; - const session = await InterviewSession.findOne({ id }); + // Validate session ID format + if (!isValidObjectId(id)) { + return NextResponse.json({ error: 'Invalid session ID' }, { status: 400 }); + } + + // Authenticate user + const authHeader = req.headers.get('Authorization'); + const authenticatedUser = await getAuthenticatedUser(authHeader); + if (!authenticatedUser) { + return NextResponse.json({ error: 'Unauthorized' }, { status: 401 }); + } + + await dbConnect(); + + // Find user in database + const user = await User.findOne({ firebaseUid: authenticatedUser.uid }); + if (!user) { + return NextResponse.json({ error: 'User not found' }, { status: 404 }); + } + + // Find session and verify ownership + const session = await InterviewSession.findOne({ _id: id, userId: user._id }); if (!session) { return NextResponse.json({ error: 'Session not found' }, { status: 404 }); } @@ -18,7 +40,7 @@ export async function POST( if (!session.aiMessages) session.aiMessages = []; // Idempotency check: Have we already sent it? - const alreadySent = session.aiMessages.some((m: { role: string; content: string }) => + const alreadySent = session.aiMessages.some((m: { role: string; content: string }) => m.role === 'interviewer' && m.content.includes("Final Validation Phase") ); @@ -39,4 +61,4 @@ export async function POST( console.error('Final Validation Error:', error); return NextResponse.json({ error: 'Internal server error' }, { status: 500 }); } -} +} \ No newline at end of file diff --git a/app/interview/[id]/page.tsx b/app/interview/[id]/page.tsx index 29559b2..6940e9e 100644 --- a/app/interview/[id]/page.tsx +++ b/app/interview/[id]/page.tsx @@ -101,22 +101,30 @@ export default function InterviewCanvasPage({ params }: PageProps) { onConstraintChange: handleConstraintChange }); + // Extract stable setter from ai object + const { setMessages } = ai; + // Final Validation Phase Trigger automatically checks and dispatches. useEffect(() => { if (!finalValidationTriggered && timer.minutes !== undefined && timer.minutes <= 10 && session?.status === 'in_progress') { setFinalValidationTriggered(true); - + authFetch(`/api/interview/${id}/final-validation`, { method: 'POST' }) - .then(res => res.json()) + .then(async (res) => { + if (!res.ok) { + throw new Error(await res.text() || res.statusText); + } + return res.json(); + }) .then(data => { if (data.success && data.messages) { - if (ai.setMessages) ai.setMessages(data.messages); + if (setMessages) setMessages(data.messages); setIsInterviewPanelOpen(true); } }) .catch(err => console.error('Final validation trigger failed:', err)); } - }, [timer.minutes, finalValidationTriggered, session?.status, id, ai]); + }, [timer.minutes, finalValidationTriggered, session?.status, id, setMessages]); // Fetch session data const fetchSession = useCallback(async () => { @@ -452,4 +460,4 @@ export default function InterviewCanvasPage({ params }: PageProps) { )} ); -} +} \ No newline at end of file diff --git a/components/interview/ChaosTimer.tsx b/components/interview/ChaosTimer.tsx index 8916f6d..022c111 100644 --- a/components/interview/ChaosTimer.tsx +++ b/components/interview/ChaosTimer.tsx @@ -21,7 +21,7 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) { const calculateRemaining = useCallback(() => { if (constraint.failedAt) return { remaining: 0, mode: 'failed' as const }; - + const now = Date.now(); if (constraint.overtimeAt) { const overtimeEnd = new Date(constraint.overtimeAt).getTime() + PENALTY_MS; @@ -30,24 +30,22 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) { const normalEnd = new Date(constraint.introducedAt).getTime() + WARNING_MS; const remaining = normalEnd - now; - + if (remaining <= 0) { // It hit 0 naturally but we haven't received DB overtime flag yet return { remaining: 0, mode: 'normal' as const }; } return { remaining: Math.ceil(remaining / 1000), mode: 'normal' as const }; - }, [constraint]); + }, [constraint, PENALTY_MS, WARNING_MS]); const [state, setState] = useState(() => calculateRemaining()); - - // To prevent spamming the effect while waiting for DB response + + // Prevent duplicate timeout callbacks while waiting for DB response. + // These refs track whether we've already sent a warning or penalty request. const hasTriggeredWarning = useRef(false); const hasTriggeredPenalty = useRef(false); useEffect(() => { - // Reset triggers if DB fields update - if (constraint.overtimeAt) hasTriggeredWarning.current = false; - if (constraint.failedAt) hasTriggeredPenalty.current = false; const interval = setInterval(() => { const current = calculateRemaining(); @@ -95,4 +93,4 @@ export function ChaosTimer({ constraint, onTimeout }: ChaosTimerProps) { ); -} +} \ No newline at end of file