fix: XSS event delegation, match btn class, CSS height/mobile, server validation

This commit is contained in:
Jeremy Brandenburger
2026-03-30 14:07:36 +02:00
parent 111f93da44
commit 9995d41c49
4 changed files with 51 additions and 22 deletions
+38 -18
View File
@@ -298,7 +298,7 @@ function updateDashboardOnline(online) {
`<div class="online-user">
<span class="online-dot"></span>
<span style="flex:1;font-family:var(--font-mono)">${esc(u)}</span>
<button class="btn btn-sm btn-accent" onclick="sendChallenge('${esc(u)}')">⚔ Challenge</button>
<button class="btn btn-sm btn-accent" data-action="challenge" data-user="${esc(u)}">⚔ Challenge</button>
</div>`
).join('');
}
@@ -328,7 +328,7 @@ function renderCategoryFilters() {
el.innerHTML = cats.map(cat => {
const active = state.practice.selectedCats.size === 0 || state.practice.selectedCats.has(cat);
return `<button class="cat-btn ${active ? 'active' : ''}"
onclick="toggleCategory('${esc(cat)}')">${esc(cat)}</button>`;
data-action="toggle-cat" data-cat="${esc(cat)}">${esc(cat)}</button>`;
}).join('');
}
@@ -378,6 +378,7 @@ function getPool() {
function nextStratagem() {
const pool = getPool();
if (pool.length === 0) { showPracticeIdle(); return; }
state.practice.current = pool[Math.floor(Math.random() * pool.length)];
state.practice.progress = 0;
state.practice.timeLeft = 30;
@@ -505,7 +506,7 @@ function updateLobbyView() {
`<div class="lobby-player">
<span class="online-dot"></span>
<span class="player-name">${esc(u)}</span>
<button class="btn btn-sm btn-accent" onclick="sendChallenge('${esc(u)}')">⚔ Challenge</button>
<button class="btn btn-sm btn-accent" data-action="challenge" data-user="${esc(u)}">⚔ Challenge</button>
</div>`
).join('');
}
@@ -520,8 +521,8 @@ function updateLobbyView() {
challEl.innerHTML = inc.map(from =>
`<div class="challenge-item">
<span style="flex:1"><strong>${esc(from)}</strong> challenges you to a duel!</span>
<button class="btn btn-sm btn-accent" onclick="acceptChallenge('${esc(from)}')">Accept</button>
<button class="btn btn-sm btn-muted" onclick="declineChallenge('${esc(from)}')">Decline</button>
<button class="btn btn-sm btn-accent" data-action="accept" data-user="${esc(from)}">Accept</button>
<button class="btn btn-sm btn-muted" data-action="decline" data-user="${esc(from)}">Decline</button>
</div>`
).join('');
}
@@ -573,7 +574,7 @@ function renderMatchWaiting() {
const readyBtn = document.getElementById('match-ready-btn');
readyBtn.textContent = 'READY';
readyBtn.disabled = false;
readyBtn.style.display = 'inline-flex';
readyBtn.classList.remove('hidden');
}
function renderMatchScores() {
@@ -594,7 +595,7 @@ function renderMatchRound() {
setText('match-status', m.current.name);
setText('match-category', m.current.category);
document.getElementById('match-round-area').classList.remove('hidden');
document.getElementById('match-ready-btn').style.display = 'none';
document.getElementById('match-ready-btn').classList.add('hidden');
renderArrows('match-me-sequence', m.current.sequence, 0);
renderArrows('match-opp-sequence', m.current.sequence, 0);
@@ -629,7 +630,7 @@ function renderRoundResult(winner) {
const btn = document.getElementById('match-ready-btn');
btn.textContent = 'Ready for next round';
btn.disabled = false;
btn.style.display = 'inline-flex';
btn.classList.remove('hidden');
setText('match-category', '');
}, 1600);
}
@@ -639,8 +640,8 @@ function renderMatchEnd(winner) {
setText('match-status', won ? '🏆 MATCH WON!' : '☠ MATCH LOST');
renderMatchScores();
document.getElementById('match-round-area').classList.add('hidden');
document.getElementById('match-ready-btn').style.display = 'none';
setTimeout(() => showView('lobby'), 3000);
document.getElementById('match-ready-btn').classList.add('hidden');
setTimeout(() => { if (state.currentView === 'match') showView('lobby'); }, 3000);
}
function leaveMatch() {
@@ -690,7 +691,7 @@ function renderAdminUsers(users) {
<span class="user-role badge-${u.role}">${u.role}</span>
${u.mustChange ? '<span class="badge-warning">temp pw</span>' : ''}
${u.username !== state.user.user
? `<button class="btn btn-sm btn-danger" onclick="deleteUser('${esc(u.username)}')">Delete</button>`
? `<button class="btn btn-sm btn-danger" data-action="delete-user" data-user="${esc(u.username)}">Delete</button>`
: ''}
</div>`
).join('');
@@ -718,7 +719,7 @@ async function createUser() {
}
async function deleteUser(username) {
if (!confirm('Delete "' + username + '"? This cannot be undone.')) return;
if (!confirm(`Delete user "${username}"? This cannot be undone.`)) return;
try {
await api('DELETE', '/users/' + encodeURIComponent(username));
loadAdmin();
@@ -727,6 +728,22 @@ async function deleteUser(username) {
}
}
// ── Event delegation (replaces inline onclick for user-data actions) ──────────
document.addEventListener('click', (e) => {
const btn = e.target.closest('[data-action]');
if (!btn) return;
const action = btn.dataset.action;
const user = btn.dataset.user;
const cat = btn.dataset.cat;
if (action === 'challenge' && user) sendChallenge(user);
if (action === 'accept' && user) acceptChallenge(user);
if (action === 'decline' && user) declineChallenge(user);
if (action === 'delete-user' && user) deleteUser(user);
if (action === 'toggle-cat' && cat) toggleCategory(cat);
});
// ── Keyboard input ────────────────────────────────────────────────────────────
document.addEventListener('keydown', (e) => {
const MAP = { ArrowUp: 'up', ArrowDown: 'down', ArrowLeft: 'left', ArrowRight: 'right' };
@@ -762,16 +779,19 @@ function setText(id, value) {
function showToast(msg) {
const container = document.getElementById('toast-container');
const toast = document.createElement('div');
toast.className = 'toast';
// Limit simultaneous toasts to avoid stacking
if (container.children.length >= 3) container.firstChild?.remove();
const toast = document.createElement('div');
toast.className = 'toast';
toast.textContent = msg;
container.appendChild(toast);
requestAnimationFrame(() => {
requestAnimationFrame(() => toast.classList.add('show'));
});
requestAnimationFrame(() => requestAnimationFrame(() => toast.classList.add('show')));
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => toast.remove(), 300);
toast.addEventListener('transitionend', () => toast.remove(), { once: true });
}, 3200);
}