From 74737568b0606ee9d13f58136a23122a66c058d0 Mon Sep 17 00:00:00 2001 From: marc Date: Fri, 19 Jun 2026 22:33:37 +0200 Subject: [PATCH] Fix broken action buttons and add delete confirmation modal MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - shares.js, users.js: JSON.stringify() inside onclick="..." attributes produced unescaped " chars that broke HTML attribute parsing — all dynamic onclick JSON now passed through esc() so the browser decodes " back to " before executing the handler - Replace browser confirm() dialogs with a styled Bootstrap modal (#deleteModal) for share and user deletion — reusable across all tabs - users.js: rename shadow variable 'confirm' → 'pwConfirm' to avoid shadowing the now-removed confirm() call - main.js: auto-refresh now reloads shares and users every 30 s, not just the dashboard Co-Authored-By: Claude Sonnet 4.6 --- static/js/main.js | 2 +- static/js/shares.js | 73 ++++++++++++++++++++++++++------------------ static/js/users.js | 43 +++++++++++++++++--------- templates/index.html | 19 ++++++++++++ 4 files changed, 92 insertions(+), 45 deletions(-) diff --git a/static/js/main.js b/static/js/main.js index 94ca235..b26e8db 100644 --- a/static/js/main.js +++ b/static/js/main.js @@ -1,4 +1,4 @@ loadDashboard(); loadShares(); loadUsers(); -setInterval(loadDashboard, 30_000); +setInterval(() => { loadDashboard(); loadShares(); loadUsers(); }, 30_000); diff --git a/static/js/shares.js b/static/js/shares.js index 8bd1f76..6e6603a 100644 --- a/static/js/shares.js +++ b/static/js/shares.js @@ -9,21 +9,26 @@ async function loadShares() { } else { sb.innerHTML = shares.smb.map(s => { const enabled = s.enabled !== false; - const statusDot = ``; + const dot = ``; + // esc() HTML-encodes JSON so " inside onclick="..." doesn't break the attribute + const shareJson = esc(JSON.stringify(s)); + const nameJson = esc(JSON.stringify(s.name)); + const pathJson = esc(JSON.stringify(s.path)); + const usersJson = esc(JSON.stringify(s.valid_users)); return ` ${esc(s.name)} ${esc(s.path)} ${esc(s.valid_users || '—')} - ${statusDot} + ${dot} + onclick="openEditModal('smb',${shareJson})">Edit + onclick="openQuotaModal('smb',${nameJson},${pathJson},${usersJson})">Quota + onclick="toggleShare('smb',${nameJson},${!enabled})">${enabled ? 'Disable' : 'Enable'} + onclick="deleteShare('smb',${nameJson})">Delete `; }).join(''); @@ -35,20 +40,22 @@ async function loadShares() { } else { nb.innerHTML = shares.nfs.map(s => { const enabled = s.enabled !== false; - const statusDot = ``; + const dot = ``; + const shareJson = esc(JSON.stringify(s)); + const pathJson = esc(JSON.stringify(s.path)); return ` ${esc(s.path)} ${esc(s.clients)} - ${statusDot} + ${dot} + onclick="openEditModal('nfs',${shareJson})">Edit + onclick="openQuotaModal('nfs',null,${pathJson},null)">Quota + onclick="toggleShare('nfs',${pathJson},${!enabled})">${enabled ? 'Disable' : 'Enable'} + onclick="deleteShare('nfs',${pathJson})">Delete `; }).join(''); @@ -173,25 +180,33 @@ async function submitEdit() { // ===== DELETE ===== -async function deleteShare(type, identifier) { +function deleteShare(type, identifier) { const label = type === 'smb' ? `SMB share "${identifier}"` : `NFS export "${identifier}"`; - if (!confirm(`Delete ${label}? This cannot be undone.`)) return; + document.getElementById('deleteModalMsg').textContent = + `Are you sure you want to delete ${label}? This cannot be undone.`; - let res; - if (type === 'smb') { - res = await apiFetch(`/api/share/smb/${encodeURIComponent(identifier)}`, { method: 'DELETE' }); - } else { - res = await apiFetch('/api/share/nfs', { - method: 'DELETE', headers: {'Content-Type': 'application/json'}, - body: JSON.stringify({path: identifier}), - }); - } - - if (!res) return; - const data = await res.json(); - if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); - showToast(`${label} deleted.`); - loadShares(); loadDashboard(); + const modal = new bootstrap.Modal(document.getElementById('deleteModal')); + const btn = document.getElementById('deleteModalConfirm'); + const handler = async () => { + btn.removeEventListener('click', handler); + modal.hide(); + let res; + if (type === 'smb') { + res = await apiFetch(`/api/share/smb/${encodeURIComponent(identifier)}`, { method: 'DELETE' }); + } else { + res = await apiFetch('/api/share/nfs', { + method: 'DELETE', headers: {'Content-Type': 'application/json'}, + body: JSON.stringify({path: identifier}), + }); + } + if (!res) return; + const data = await res.json(); + if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); + showToast(`${label} deleted.`); + loadShares(); loadDashboard(); + }; + btn.addEventListener('click', handler); + modal.show(); } // ===== TOGGLE ===== diff --git a/static/js/users.js b/static/js/users.js index ed8a9f2..8d30007 100644 --- a/static/js/users.js +++ b/static/js/users.js @@ -7,8 +7,10 @@ async function loadUsers() { tb.innerHTML = `No users found.`; return; } - tb.innerHTML = users.map(u => ` - + tb.innerHTML = users.map(u => { + // esc() HTML-encodes JSON so " inside onclick="..." doesn't break the attribute + const nameJson = esc(JSON.stringify(u.name)); + return ` ${esc(u.name)} ${u.uid} @@ -19,11 +21,12 @@ async function loadUsers() { + onclick="openPwModal(${nameJson})">Password + onclick="confirmDeleteUser(${nameJson})">Delete - `).join(''); + `; + }).join(''); } async function createUser() { @@ -44,14 +47,24 @@ async function createUser() { loadUsers(); } -async function deleteUser(name) { - if (!confirm(`Delete user "${name}"? This cannot be undone.`)) return; - const res = await apiFetch(`/api/users/${encodeURIComponent(name)}`, { method: 'DELETE' }); - if (!res) return; - const data = await res.json(); - if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); - showToast(`User "${name}" deleted.`); - loadUsers(); +function confirmDeleteUser(name) { + document.getElementById('deleteModalMsg').textContent = + `Are you sure you want to delete user "${name}"? This removes both the system account and SMB access.`; + + const modal = new bootstrap.Modal(document.getElementById('deleteModal')); + const btn = document.getElementById('deleteModalConfirm'); + const handler = async () => { + btn.removeEventListener('click', handler); + modal.hide(); + const res = await apiFetch(`/api/users/${encodeURIComponent(name)}`, { method: 'DELETE' }); + if (!res) return; + const data = await res.json(); + if (!res.ok) return showToast('Error: ' + (data.error || res.status), 'danger'); + showToast(`User "${name}" deleted.`); + loadUsers(); + }; + btn.addEventListener('click', handler); + modal.show(); } function openPwModal(name) { @@ -64,9 +77,9 @@ function openPwModal(name) { async function submitPassword() { const name = document.getElementById('pwUsername').value; const pw = document.getElementById('pwNew').value; - const confirm = document.getElementById('pwConfirm').value; + const pwConfirm = document.getElementById('pwConfirm').value; if (pw.length < 8) return showToast('Password must be at least 8 characters.', 'danger'); - if (pw !== confirm) return showToast('Passwords do not match.', 'danger'); + if (pw !== pwConfirm) return showToast('Passwords do not match.', 'danger'); const res = await apiFetch(`/api/users/${encodeURIComponent(name)}/password`, { method: 'POST', headers: {'Content-Type': 'application/json'}, diff --git a/templates/index.html b/templates/index.html index df1b649..289036b 100644 --- a/templates/index.html +++ b/templates/index.html @@ -65,6 +65,25 @@ + + +
MicroNAS