Skip to content

Commit be9df05

Browse files
committed
feat: enhance SQL challenge interface with improved button layout and error handling notifications
1 parent 2ca4d1d commit be9df05

6 files changed

Lines changed: 143 additions & 84 deletions

File tree

platform/CTFd/plugins/sql_challenges/assets/update.html

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ <h5>Test Results:</h5>
8383
The state of the challenge (visible, hidden)
8484
</small>
8585
</label>
86-
<select class="form-control challenge-state" name="state">
86+
<select class="form-control" name="state">
8787
<option value="visible" {% if challenge.state == "visible" %}selected{% endif %}>Visible</option>
8888
<option value="hidden" {% if challenge.state == "hidden" %}selected{% endif %}>Hidden</option>
8989
</select>
@@ -97,6 +97,11 @@ <h5>Test Results:</h5>
9797
</label>
9898
<input type="number" class="form-control challenge-max-attempts" name="max_attempts" placeholder="0" value="{{ challenge.max_attempts }}">
9999
</div>
100-
101-
<button type="submit" class="btn btn-primary">Update</button>
100+
101+
<div class="d-flex gap-8">
102+
<button type="submit" class="btn btn-primary mr-2">Update</button>
103+
<a href="/challenges/sql/{{ challenge.id }}" class="btn btn-info" target="_blank">
104+
<i class="fas fa-external-link-alt me-1"></i> View Challenge Page
105+
</a>
106+
</div>
102107
</form>

platform/CTFd/themes/ddps/assets/js/sql_challenge.js

Lines changed: 74 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -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
198228
function 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

Comments
 (0)