@@ -9,17 +9,34 @@ export async function GET(
99 { params } : { params : { id : string } }
1010) {
1111 const session = await getServerSession ( authOptions ) ;
12-
1312 if ( ! session ?. user ?. id ) {
1413 return NextResponse . json ( { error : 'Unauthorized' } , { status : 401 } ) ;
1514 }
1615
1716 const userId = session . user . id ;
1817 const exerciseId = params . id ;
1918
20- // Look up or create the session for this exercise + user
19+ // Check exercise-level start_time BEFORE creating a session
20+ // This way participants who haven't started yet are blocked at the exercise level
21+ const exerciseRows = await sql `
22+ SELECT start_time, end_time, duration_limit, question_count
23+ FROM exercises WHERE id = ${ exerciseId } LIMIT 1
24+ ` ;
25+ if ( exerciseRows . length === 0 ) {
26+ return NextResponse . json ( { error : 'Exercise not found' } , { status : 404 } ) ;
27+ }
28+ const exercise = exerciseRows [ 0 ] ;
29+
30+ if ( exercise . start_time && new Date ( ) < new Date ( exercise . start_time as string ) ) {
31+ return NextResponse . json (
32+ { error : 'Session not yet open' , opens_at : exercise . start_time } ,
33+ { status : 423 }
34+ ) ;
35+ }
36+
37+ // Look up or create the session
2138 let rows = await sql `
22- SELECT s.id, s.start_time, s. end_time, s.duration_limit,
39+ SELECT s.id, s.end_time, s.duration_limit,
2340 s.started_at, s.closed_at, s.current_question_index,
2441 e.question_count
2542 FROM sessions s
@@ -30,69 +47,50 @@ export async function GET(
3047 ` ;
3148
3249 if ( rows . length === 0 ) {
33- // Create a new session, inheriting timing from the exercise
50+ // Create session — inherit end_time and duration_limit but NOT start_time
51+ // (start_time is checked at the exercise level above)
3452 const created = await sql `
35- INSERT INTO sessions (exercise_id, user_id, started_at, current_question_index, start_time, end_time, duration_limit)
53+ INSERT INTO sessions (exercise_id, user_id, started_at, current_question_index, end_time, duration_limit)
3654 SELECT
3755 ${ exerciseId } ,
3856 ${ userId } ,
3957 now(),
4058 0,
41- e.start_time,
4259 e.end_time,
4360 e.duration_limit
4461 FROM exercises e
4562 WHERE e.id = ${ exerciseId }
46- RETURNING id, start_time, end_time, duration_limit,
47- started_at, closed_at, current_question_index
48- ` ;
49-
50- const exerciseRows = await sql `
51- SELECT question_count FROM exercises WHERE id = ${ exerciseId }
63+ RETURNING id, end_time, duration_limit, started_at, closed_at, current_question_index
5264 ` ;
5365
5466 rows = created . map ( ( r ) => ( {
5567 ...r ,
56- question_count : exerciseRows [ 0 ] ? .question_count ?? 0 ,
68+ question_count : exercise . question_count ?? 0 ,
5769 } ) ) ;
5870 }
5971
6072 const row = rows [ 0 ] ;
6173
62- // 423 if start_time is set and now() < start_time
63- if ( row . start_time && new Date ( ) < new Date ( row . start_time as string ) ) {
64- return NextResponse . json (
65- { error : 'Session not yet open' , opens_at : row . start_time } ,
66- { status : 423 }
67- ) ;
68- }
69-
7074 // 410 if session is closed
7175 if ( row . closed_at ) {
7276 return NextResponse . json ( { error : 'Session closed' } , { status : 410 } ) ;
7377 }
7478
7579 const now = new Date ( ) ;
7680
77- // Auto-close if end_time has passed
78- if ( row . end_time && now > new Date ( row . end_time as string ) ) {
79- await sql `
80- UPDATE sessions SET closed_at = now()
81- WHERE id = ${ row . id } AND closed_at IS NULL
82- ` ;
81+ // Auto-close if end_time has passed (check both session and exercise end_time)
82+ const effectiveEndTime = ( row . end_time || exercise . end_time ) as string | null ;
83+ if ( effectiveEndTime && now > new Date ( effectiveEndTime ) ) {
84+ await sql `UPDATE sessions SET closed_at = now() WHERE id = ${ row . id } AND closed_at IS NULL` ;
8385 return NextResponse . json ( { error : 'Session closed' } , { status : 410 } ) ;
8486 }
8587
86- // Auto-close if duration_limit has been exceeded
88+ // Auto-close if duration_limit exceeded
8789 if ( row . duration_limit && row . started_at ) {
8890 const durationSeconds = parseIntervalToSeconds ( row . duration_limit ) ;
8991 const startedAt = new Date ( row . started_at as string ) . getTime ( ) ;
90- const expiresAt = startedAt + durationSeconds * 1000 ;
91- if ( now . getTime ( ) > expiresAt ) {
92- await sql `
93- UPDATE sessions SET closed_at = now()
94- WHERE id = ${ row . id } AND closed_at IS NULL
95- ` ;
92+ if ( now . getTime ( ) > startedAt + durationSeconds * 1000 ) {
93+ await sql `UPDATE sessions SET closed_at = now() WHERE id = ${ row . id } AND closed_at IS NULL` ;
9694 return NextResponse . json ( { error : 'Session closed' } , { status : 410 } ) ;
9795 }
9896 }
@@ -103,41 +101,30 @@ export async function GET(
103101 if ( row . duration_limit && row . started_at ) {
104102 const durationSeconds = parseIntervalToSeconds ( row . duration_limit ) ;
105103 const startedAt = new Date ( row . started_at as string ) . getTime ( ) ;
106- const expiresAt = startedAt + durationSeconds * 1000 ;
107- remainingSeconds = Math . floor ( ( expiresAt - Date . now ( ) ) / 1000 ) ;
104+ remainingSeconds = Math . floor ( ( startedAt + durationSeconds * 1000 - Date . now ( ) ) / 1000 ) ;
108105 }
109106
110- if ( row . end_time ) {
111- const fromEndTime = Math . floor (
112- ( new Date ( row . end_time as string ) . getTime ( ) - Date . now ( ) ) / 1000
113- ) ;
107+ if ( effectiveEndTime ) {
108+ const fromEndTime = Math . floor ( ( new Date ( effectiveEndTime ) . getTime ( ) - Date . now ( ) ) / 1000 ) ;
114109 remainingSeconds = remainingSeconds === null ? fromEndTime : Math . min ( remainingSeconds , fromEndTime ) ;
115110 }
116111
117- const warningLowTime =
118- remainingSeconds !== null && remainingSeconds < 300 ;
119-
120- // Query per-question submission statuses
121112 const submissions = await sql `
122113 SELECT question_index, is_final,
123114 (response_text IS NOT NULL AND response_text <> '') AS has_draft
124- FROM submissions
125- WHERE session_id = ${ row . id }
115+ FROM submissions WHERE session_id = ${ row . id }
126116 ` ;
127117
128- const questionStatuses = submissions . map ( ( s ) => ( {
129- question_index : s . question_index ,
130- has_draft : Boolean ( s . has_draft ) ,
131- is_final : Boolean ( s . is_final ) ,
132- } ) ) ;
133-
134118 return NextResponse . json ( {
135119 session_id : row . id ,
136- current_question_index : row . current_question_index ,
137- question_count : row . question_count ,
120+ current_question_index : row . current_question_index as number ,
121+ question_count : row . question_count as number ,
138122 remaining_seconds : remainingSeconds ,
139- warning_low_time : warningLowTime ,
140- question_statuses : questionStatuses ,
123+ warning_low_time : remainingSeconds !== null && remainingSeconds < 300 ,
124+ question_statuses : submissions . map ( ( s ) => ( {
125+ question_index : s . question_index ,
126+ has_draft : Boolean ( s . has_draft ) ,
127+ is_final : Boolean ( s . is_final ) ,
128+ } ) ) ,
141129 } ) ;
142130}
143-
0 commit comments