@@ -169,23 +169,23 @@ function showAutoSaveNotification(message) {
169169 const notification = document . createElement ( 'div' ) ;
170170 notification . className = 'alert alert-info fade show position-fixed' ;
171171 notification . style . cssText = 'top: 70px; right: 20px; z-index: 1050; max-width: 350px; padding: 0.75rem 2.5rem 0.75rem 1rem; position: relative;' ;
172-
172+
173173 // Create message content
174174 const messageContent = document . createElement ( 'div' ) ;
175175 messageContent . style . cssText = 'display: flex; align-items: center;' ;
176176 messageContent . innerHTML = `<i class="fas fa-save me-2"></i><span>${ message } </span>` ;
177-
177+
178178 // Create close button
179179 const closeButton = document . createElement ( 'button' ) ;
180180 closeButton . type = 'button' ;
181181 closeButton . className = 'btn-close btn-sm' ;
182182 closeButton . style . cssText = 'position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); padding: 0.25rem; font-size: 0.875rem;' ;
183183 closeButton . onclick = function ( ) { notification . remove ( ) ; } ;
184-
184+
185185 notification . appendChild ( messageContent ) ;
186186 notification . appendChild ( closeButton ) ;
187187 document . body . appendChild ( notification ) ;
188-
188+
189189 // Auto-hide after 3 seconds
190190 setTimeout ( ( ) => {
191191 if ( notification . parentNode ) {
@@ -194,6 +194,36 @@ function showAutoSaveNotification(message) {
194194 } , 3000 ) ;
195195}
196196
197+ // Show error toast notification
198+ function showErrorToast ( message ) {
199+ const notification = document . createElement ( 'div' ) ;
200+ notification . className = 'alert alert-danger fade show position-fixed' ;
201+ notification . style . cssText = 'top: 70px; right: 20px; z-index: 1050; max-width: 400px; padding: 0.75rem 2.5rem 0.75rem 1rem; position: relative;' ;
202+
203+ // Create message content
204+ const messageContent = document . createElement ( 'div' ) ;
205+ messageContent . style . cssText = 'display: flex; align-items: center;' ;
206+ messageContent . innerHTML = `<i class="fas fa-exclamation-triangle me-2"></i><span>${ escapeHtml ( message ) } </span>` ;
207+
208+ // Create close button
209+ const closeButton = document . createElement ( 'button' ) ;
210+ closeButton . type = 'button' ;
211+ closeButton . className = 'btn-close btn-sm' ;
212+ closeButton . style . cssText = 'position: absolute; top: 50%; right: 0.5rem; transform: translateY(-50%); padding: 0.25rem; font-size: 0.875rem;' ;
213+ closeButton . onclick = function ( ) { notification . remove ( ) ; } ;
214+
215+ notification . appendChild ( messageContent ) ;
216+ notification . appendChild ( closeButton ) ;
217+ document . body . appendChild ( notification ) ;
218+
219+ // Auto-hide after 5 seconds
220+ setTimeout ( ( ) => {
221+ if ( notification . parentNode ) {
222+ notification . remove ( ) ;
223+ }
224+ } , 5000 ) ;
225+ }
226+
197227// Clear saved code when challenge is solved
198228function clearSavedCode ( ) {
199229 const challengeId = document . getElementById ( 'challenge-id' ) . value ;
@@ -264,15 +294,22 @@ async function executeSQLQuery() {
264294 } ;
265295 // Request body prepared
266296
297+ // Create abort controller for timeout
298+ const controller = new AbortController ( ) ;
299+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 20000 ) ; // 20 second timeout
300+
267301 const response = await fetch ( '/api/v1/challenges/attempt' , {
268302 method : 'POST' ,
269303 headers : {
270304 'Content-Type' : 'application/json' ,
271305 'CSRF-Token' : init . csrfNonce
272306 } ,
273- body : JSON . stringify ( requestBody )
307+ body : JSON . stringify ( requestBody ) ,
308+ signal : controller . signal
274309 } ) ;
275310
311+ clearTimeout ( timeoutId ) ;
312+
276313 // Response received
277314 if ( ! response . ok ) {
278315 console . error ( 'Response not OK:' , response . statusText ) ;
@@ -289,15 +326,15 @@ async function executeSQLQuery() {
289326 } catch ( e ) {
290327 console . error ( 'Failed to parse response as JSON:' , e ) ;
291328 console . error ( 'Response was:' , responseText ) ;
292- alert ( 'Server returned invalid JSON response' ) ;
329+ showErrorToast ( 'Server returned invalid JSON response' ) ;
293330 return ;
294331 }
295332 // Result parsed
296333
297334 // Check if result has the expected structure
298335 if ( ! result || typeof result !== 'object' ) {
299336 console . error ( 'Invalid result structure:' , result ) ;
300- alert ( 'Invalid response from server' ) ;
337+ showErrorToast ( 'Invalid response from server' ) ;
301338 return ;
302339 }
303340
@@ -315,9 +352,14 @@ async function executeSQLQuery() {
315352 }
316353
317354 } catch ( error ) {
318- console . error ( 'Error executing query:' , error ) ;
319- console . error ( 'Error details:' , error . message , error . stack ) ;
320- alert ( 'Error executing query. Please check console for details.' ) ;
355+ if ( error . name === 'AbortError' ) {
356+ console . error ( 'Request timed out after 20 seconds' ) ;
357+ showErrorToast ( 'Request timed out. The server took too long to respond. Please try again.' ) ;
358+ } else {
359+ console . error ( 'Error executing query:' , error ) ;
360+ console . error ( 'Error details:' , error . message , error . stack ) ;
361+ showErrorToast ( 'Error executing query. Please try again.' ) ;
362+ }
321363 }
322364}
323365
@@ -333,7 +375,7 @@ async function submitSQLChallenge() {
333375 alert ( 'Please enter a SQL query' ) ;
334376 return ;
335377 }
336-
378+
337379 // Check deadline before submitting
338380 const deadlineElement = document . getElementById ( 'deadline-time' ) ;
339381 if ( deadlineElement ) {
@@ -355,8 +397,12 @@ async function submitSQLChallenge() {
355397 }
356398 }
357399 }
358-
400+
359401 try {
402+ // Create abort controller for timeout
403+ const controller = new AbortController ( ) ;
404+ const timeoutId = setTimeout ( ( ) => controller . abort ( ) , 20000 ) ; // 20 second timeout
405+
360406 const response = await fetch ( '/api/v1/challenges/attempt' , {
361407 method : 'POST' ,
362408 headers : {
@@ -368,16 +414,19 @@ async function submitSQLChallenge() {
368414 submission : submission ,
369415 user_id : userId ,
370416 user_name : userName
371- } )
417+ } ) ,
418+ signal : controller . signal
372419 } ) ;
373-
420+
421+ clearTimeout ( timeoutId ) ;
422+
374423 const result = await response . json ( ) ;
375424 // Submit result received
376-
425+
377426 // Check if result has the expected structure
378427 if ( ! result || typeof result !== 'object' ) {
379428 console . error ( 'Invalid result structure:' , result ) ;
380- alert ( 'Invalid response from server' ) ;
429+ showErrorToast ( 'Invalid response from server' ) ;
381430 return ;
382431 }
383432
@@ -389,7 +438,7 @@ async function submitSQLChallenge() {
389438 submit_status : submitStatus
390439 } )
391440 }
392-
441+
393442 // CTFd API returns {success: bool, data: {...}} structure
394443 if ( ! result . hasOwnProperty ( 'data' ) ) {
395444 // Wrapping result in CTFd format
@@ -401,10 +450,15 @@ async function submitSQLChallenge() {
401450 } else {
402451 displayResult ( result , false ) ;
403452 }
404-
453+
405454 } catch ( error ) {
406- console . error ( 'Error submitting challenge:' , error ) ;
407- alert ( 'Error submitting challenge. Please try again.' ) ;
455+ if ( error . name === 'AbortError' ) {
456+ console . error ( 'Request timed out after 20 seconds' ) ;
457+ showErrorToast ( 'Request timed out. The server took too long to respond. Please try again.' ) ;
458+ } else {
459+ console . error ( 'Error submitting challenge:' , error ) ;
460+ showErrorToast ( 'Error submitting challenge. Please try again.' ) ;
461+ }
408462 }
409463}
410464
0 commit comments