fix: XSS event delegation, match btn class, CSS height/mobile, server validation
This commit is contained in:
+38
-18
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user