Skip to content

Commit 12ed1c8

Browse files
committed
Secure more endpoints, bug fixes
1 parent aa1b0c1 commit 12ed1c8

3 files changed

Lines changed: 112 additions & 62 deletions

File tree

admin/upload.html

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -349,6 +349,8 @@
349349
if (platform) item.setMetadata('platform', platform);
350350
if (uploaderQuestionId && (uploader.querySelector('[data-question-number]').value || uploaderQuestion)) item.setMetadata('startingQuestionId', uploaderQuestionId);
351351
if (uploaderQuestionId && (uploader.querySelector('[data-question-number]').value || uploaderQuestion)) item.setMetadata('startingQuestion', uploader.querySelector('[data-question-number]').value || uploaderQuestion);
352+
item.setMetadata('usr', storage.get('usr') || '');
353+
item.setMetadata('pwd', storage.get('pwd') || '');
352354
});
353355
pond.on('processfilestart', (file) => {
354356
if (uploaderQuestionId && uploaderQuestion && !file.getMetadata('title')) {

src/checker/admin.js

Lines changed: 109 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ import "faz-quill-emoji/autoregister";
1616
const domain = ((window.location.hostname.search('check') != -1) || (window.location.hostname.search('127') != -1)) ? 'https://api.check.vssfalcons.com' : `http://${document.domain}:5000`;
1717
if (window.location.pathname.split('?')[0].endsWith('/admin')) window.location.pathname = '/admin/';
1818
const params = Object.fromEntries((new URL(location)).searchParams);
19-
const ws = new WebSocket(`${domain}/ws?${new URLSearchParams({ role: 'admin' }).toString()}`);
19+
var ws = null;
20+
var reconnectAttempts = 0;
21+
var createWebSocket;
2022

2123
var archiveTypeSelected = null;
2224
var courses = [];
@@ -103,6 +105,86 @@ try {
103105
}
104106
}
105107

108+
createWebSocket = function () {
109+
try {
110+
const usr = storage.get('usr') || '';
111+
const pwd = storage.get('pwd') || '';
112+
const params = new URLSearchParams({ role: 'admin', usr, pwd }).toString();
113+
if (ws && ((ws.readyState === WebSocket.OPEN) || (ws.readyState === WebSocket.CONNECTING))) return;
114+
if (ws) ws.close();
115+
ws = new WebSocket(`${domain}/ws?${params}`);
116+
ws.addEventListener('open', () => {
117+
reconnectAttempts = 0;
118+
});
119+
ws.addEventListener('message', (e) => {
120+
const data = JSON.parse(e.data);
121+
if (data.type === 'studentDraw') {
122+
const { sessionKey, meta, stroke } = data;
123+
if ((meta.source || 'unknown') !== 'clicker') return;
124+
if (!liveDrawingSessions[sessionKey]) createSession({ data }, sessionKey);
125+
const info = liveDrawingSessions[sessionKey];
126+
info.strokes = info.strokes || [];
127+
info.strokes.push({ stroke });
128+
renderStrokesIntoSession(sessionKey, info.strokes);
129+
info.wrapper.querySelectorAll('.meta').forEach(el => el.style.display = hideSeatCodes ? 'none' : 'block');
130+
} else if (data.type === 'sessionSync') {
131+
if (!data || !data.data || !data.data.meta || !data.data.meta.source || !data.data.meta.seatCode || !data.data.strokes || !Array.isArray(data.data.strokes) || !data.data.strokes.length) return;
132+
const sessionKey = data.sessionKey;
133+
if (!liveDrawingSessions[sessionKey]) createSession(data, sessionKey);
134+
} else if (data.type === 'studentUndo') {
135+
const { sessionKey, strokeId } = data;
136+
const info = liveDrawingSessions[sessionKey];
137+
if (!info) return;
138+
info.strokes = info.strokes || [];
139+
info.strokes = info.strokes.filter(s => {
140+
const st = s.stroke || s;
141+
if (!st) return true;
142+
if (Array.isArray(st)) return !st.some(i => (i && i.id && String(i.id) === String(strokeId)));
143+
if (st && st.id && String(st.id) === String(strokeId)) return false;
144+
return true;
145+
});
146+
renderStrokesIntoSession(sessionKey, info.strokes);
147+
} else if (data.type === 'studentClear') {
148+
const { sessionKey } = data;
149+
const info = liveDrawingSessions[sessionKey];
150+
if (!info) return;
151+
info.strokes = [];
152+
renderStrokesIntoSession(sessionKey, info.strokes || []);
153+
} else if (data.type === 'resetPeriod') {
154+
const period = String(data.period);
155+
Object.values(liveDrawingSessions).forEach(info => {
156+
const seat = info?.meta?.seatCode?.toString?.() || '';
157+
if (seat && seat.startsWith(period)) {
158+
if (info && info.canvas) {
159+
const context = info.canvas.getContext('2d');
160+
context.clearRect(0, 0, info.canvas.width, info.canvas.height);
161+
info.strokes = [];
162+
}
163+
}
164+
});
165+
}
166+
});
167+
ws.addEventListener('close', () => {
168+
console.warn('Admin websocket closed, scheduling reconnect');
169+
scheduleAdminReconnect();
170+
});
171+
ws.addEventListener('error', (err) => {
172+
console.error('Admin websocket error', err);
173+
try { ws.close(); } catch (e) { console.warn('adminWs close failed', e); }
174+
});
175+
} catch (e) {
176+
scheduleAdminReconnect();
177+
}
178+
}
179+
180+
function scheduleAdminReconnect() {
181+
reconnectAttempts = (reconnectAttempts || 0) + 1;
182+
const delay = Math.min(30000, Math.pow(2, Math.min(reconnectAttempts, 6)) * 1000);
183+
setTimeout(() => {
184+
createWebSocket();
185+
}, delay);
186+
}
187+
106188
if (!(await auth.bulkLoad(getAdminFields(), storage.get("usr"), storage.get("pwd"), true, false, () => {
107189
auth.admin(init);
108190
pollingOff();
@@ -124,7 +206,6 @@ try {
124206
settings = bulkLoad.settings || {};
125207
if (document.querySelector('.users')) {
126208
if (document.getElementById('add-user-button')) document.getElementById('add-user-button').addEventListener('click', addUserModal);
127-
128209
document.querySelector('.users').innerHTML = '<div class="row header"><span>User</span><span>Role</span><span>Partial Access</span><span>Full Access</span><span>Anonymous</span><span>Actions</span></div>';
129210
if (users.length > 0) {
130211
document.getElementById('no-users').setAttribute('hidden', '');
@@ -178,7 +259,6 @@ try {
178259
}
179260
if (document.querySelector('.passwords')) {
180261
if (document.getElementById('remove-passwords')) document.getElementById('remove-passwords').addEventListener('click', removePasswordsModal);
181-
182262
document.querySelector('.passwords').innerHTML = '<div class="row header"><span>Seat Code</span><span>Saved Settings</span><span>Actions</span></div>';
183263
if (passwords.length > 0) {
184264
document.getElementById('no-passwords').setAttribute('hidden', '');
@@ -363,6 +443,9 @@ try {
363443
document.getElementById("period-input").innerHTML += `<option value="${coursePeriod.period}">Period ${coursePeriod.period} - ${coursePeriod.name}</option>`;
364444
});
365445
if (document.getElementById('live-drawing-periods')) {
446+
courses.flatMap(course => JSON.parse(course.periods)).forEach(period => {
447+
getOrCreateGroup(period);
448+
})
366449
var currentPeriod = getExtendedPeriod();
367450
if ((currentPeriod != -1) && courses.flatMap(course => JSON.parse(course.periods).map(period => { return { period, name: course.name } })).some(coursePeriod => coursePeriod.period === (currentPeriod + 1))) {
368451
document.getElementById("period-input").value = getExtendedPeriod() + 1;
@@ -375,7 +458,7 @@ try {
375458
}
376459
if (document.getElementById('saved-live-drawings') && params && params.id) {
377460
const sessionId = params.id;
378-
const savedSession = await fetch(domain + '/draw/sessions/' + sessionId);
461+
const savedSession = await fetch(domain + '/draw/sessions/' + sessionId + '?usr=' + encodeURIComponent(storage.get('usr')) + '&pwd=' + encodeURIComponent(storage.get('pwd')));
379462
const savedSessionJSON = await savedSession.json();
380463
document.title = `${savedSessionJSON.session.name} - Virtual Checker`;
381464
document.querySelector('.section h1').innerText = savedSessionJSON.session.name;
@@ -432,7 +515,17 @@ try {
432515
ui.reloadUnsavedInputs();
433516
}
434517

435-
init();
518+
init()
519+
.then(() => {
520+
const initializeWebSocket = () => {
521+
if (typeof createWebSocket === 'function') {
522+
createWebSocket();
523+
} else {
524+
setTimeout(initializeWebSocket, 200);
525+
}
526+
};
527+
initializeWebSocket();
528+
});
436529

437530
window.addEventListener('beforeunload', function (event) {
438531
if (!ui.unsavedChanges) return;
@@ -677,6 +770,8 @@ try {
677770
body: JSON.stringify({
678771
course_id: course.id,
679772
platform: 'clicker',
773+
usr: storage.get('usr'),
774+
pwd: storage.get('pwd')
680775
}),
681776
})
682777
.then(async (r) => {
@@ -735,6 +830,8 @@ try {
735830
body: JSON.stringify({
736831
course_id: course.id,
737832
platform: 'checker',
833+
usr: storage.get('usr'),
834+
pwd: storage.get('pwd')
738835
}),
739836
})
740837
.then(async (r) => {
@@ -6536,7 +6633,7 @@ try {
65366633
goToPage(paginationSection, Math.ceil(pagination[group].total / (storage.get("rowsPerPage") ? Number(storage.get("rowsPerPage")) : pagination[group].perPage)) - 1);
65376634
}
65386635

6539-
function getOrCreateGroup(period, source) {
6636+
function getOrCreateGroup(period) {
65406637
let el = document.querySelector(`[data-period="${period}"]`);
65416638
if (el) return el;
65426639
el = document.createElement('div');
@@ -6549,7 +6646,7 @@ try {
65496646
function createSession(data, sessionKey) {
65506647
const [source, seatCode] = sessionKey.split('::');
65516648
if ((data.data.meta.source || 'unknown') !== 'clicker') return;
6552-
const sessions = getOrCreateGroup(Number(seatCode.slice(0, 1)), data.data.meta.source || 'unknown');
6649+
const sessions = getOrCreateGroup(Number(seatCode.slice(0, 1)));
65536650
const sessionDiv = document.createElement('div');
65546651
sessionDiv.className = 'session';
65556652
sessionDiv.innerHTML = `<span class="meta">${data.data.meta.seatCode}</span><div class="canvas-wrapper"></div><div class="overlays"></div>`;
@@ -6582,58 +6679,7 @@ try {
65826679
}
65836680
}
65846681

6585-
if (document.getElementById('live-drawing-periods')) ws.addEventListener('message', (e) => {
6586-
const data = JSON.parse(e.data);
6587-
if (data.type === 'studentDraw') {
6588-
const { sessionKey, meta, stroke } = data;
6589-
if ((meta.source || 'unknown') !== 'clicker') return;
6590-
if (!liveDrawingSessions[sessionKey]) createSession({ data }, sessionKey);
6591-
const info = liveDrawingSessions[sessionKey];
6592-
info.strokes = info.strokes || [];
6593-
info.strokes.push({ stroke });
6594-
renderStrokesIntoSession(sessionKey, info.strokes);
6595-
info.wrapper.querySelectorAll('.meta').forEach(el => el.style.display = hideSeatCodes ? 'none' : 'block');
6596-
} else if (data.type === 'sessionSync') {
6597-
if (!data || !data.data || !data.data.meta || !data.data.meta.source || !data.data.meta.seatCode || !data.data.strokes || !Array.isArray(data.data.strokes) || !data.data.strokes.length) return;
6598-
const sessionKey = data.sessionKey;
6599-
if (!liveDrawingSessions[sessionKey]) createSession(data, sessionKey);
6600-
} else if (data.type === 'studentClear') {
6601-
const { sessionKey } = data;
6602-
const info = liveDrawingSessions[sessionKey];
6603-
if (info) {
6604-
info.strokes = [];
6605-
renderStrokesIntoSession(sessionKey, info.strokes);
6606-
}
6607-
} else if (data.type === 'resetPeriod') {
6608-
const period = String(data.period);
6609-
Object.keys(liveDrawingSessions).forEach(sessionKey => {
6610-
const parts = sessionKey.split('::');
6611-
const seat = parts[1] || '';
6612-
if (String(seat).startsWith(period)) {
6613-
const info = liveDrawingSessions[sessionKey];
6614-
if (info && info.canvas) {
6615-
const ctx = info.canvas.getContext('2d');
6616-
ctx.clearRect(0, 0, info.canvas.width, info.canvas.height);
6617-
}
6618-
if (info) info.strokes = [];
6619-
}
6620-
});
6621-
} else if (data.type === 'studentUndo') {
6622-
const { sessionKey, strokeId } = data;
6623-
const info = liveDrawingSessions[sessionKey];
6624-
if (info && Array.isArray(info.strokes)) {
6625-
const before = info.strokes.length;
6626-
info.strokes = info.strokes.filter(s => {
6627-
const seg = s.stroke || s;
6628-
if (!seg) return true;
6629-
return String(seg.id) !== String(strokeId);
6630-
});
6631-
if (info.strokes.length !== before) {
6632-
renderStrokesIntoSession(sessionKey, info.strokes);
6633-
}
6634-
}
6635-
}
6636-
});
6682+
// Live drawing websocket messages are handled on the adminWs created by createAdminWS()
66376683

66386684
function renderStrokesIntoSession(sessionKey, strokes) {
66396685
const info = liveDrawingSessions[sessionKey];
@@ -6725,7 +6771,9 @@ try {
67256771
created: new Date().toISOString(),
67266772
period: document.getElementById('period-input').value || null,
67276773
name: `Live Drawing Session - Period ${document.getElementById('period-input').value || 0} - ${new Date().toLocaleString()}`,
6728-
}
6774+
},
6775+
usr: storage.get('usr'),
6776+
pwd: storage.get('pwd')
67296777
})
67306778
});
67316779
const saveSessionJSON = await saveSession.json();
@@ -6785,7 +6833,7 @@ try {
67856833
});
67866834

67876835
async function refreshSavedLiveDrawingSessions() {
6788-
const refreshSessions = await fetch(domain + '/draw/sessions');
6836+
const refreshSessions = await fetch(domain + '/draw/sessions?usr=' + encodeURIComponent(storage.get('usr')) + '&pwd=' + encodeURIComponent(storage.get('pwd')));
67896837
const refreshSessionsJSON = await refreshSessions.json();
67906838
document.querySelector('.saved-live-drawing-sessions').innerHTML = '';
67916839
if (refreshSessionsJSON.sessions && refreshSessionsJSON.sessions.length) {

src/design.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -435,7 +435,7 @@ div.ML__keyboard {
435435

436436
[data-button-select] > button {
437437
height: unset;
438-
min-height: 1.75em;
438+
min-height: 1.75em !important;
439439
border-radius: 0.25rem;
440440
cursor: pointer;
441441
}

0 commit comments

Comments
 (0)