Fix broken action buttons and add delete confirmation modal
- 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 <noreply@anthropic.com>
This commit is contained in:
1 parent
67dacd4044
commit
74737568b0
4 files changed
+92
-45
No files matched your search
+1
-1
@@ -1,4 +1,4 @@
|
||||
loadDashboard();
|
||||
loadShares();
|
||||
loadUsers();
|
||||
setInterval(loadDashboard, 30_000);
|
||||
setInterval(() => { loadDashboard(); loadShares(); loadUsers(); }, 30_000);
|
||||
+44
-29
@@ -9,21 +9,26 @@ async function loadShares() {
|
||||
} else {
|
||||
sb.innerHTML = shares.smb.map(s => {
|
||||
const enabled = s.enabled !== false;
|
||||
const statusDot = `<span class="dot ${enabled ? 'dot-green' : 'dot-red'}" style="margin-right:0"></span>`;
|
||||
const dot = `<span class="dot ${enabled ? 'dot-green' : 'dot-red'}" style="margin-right:0"></span>`;
|
||||
// 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 `<tr style="${enabled ? '' : 'opacity:.55'}">
|
||||
<td>${esc(s.name)}</td>
|
||||
<td><code>${esc(s.path)}</code></td>
|
||||
<td style="color:var(--muted); font-size:.87em">${esc(s.valid_users || '—')}</td>
|
||||
<td>${statusDot}</td>
|
||||
<td>${dot}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="openEditModal('smb',${JSON.stringify(s)})">Edit</button>
|
||||
onclick="openEditModal('smb',${shareJson})">Edit</button>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="openQuotaModal('smb',${JSON.stringify(s.name)},${JSON.stringify(s.path)},${JSON.stringify(s.valid_users)})">Quota</button>
|
||||
onclick="openQuotaModal('smb',${nameJson},${pathJson},${usersJson})">Quota</button>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="toggleShare('smb',${JSON.stringify(s.name)},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
||||
onclick="toggleShare('smb',${nameJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button class="btn btn-sm btn-danger"
|
||||
onclick="deleteShare('smb',${JSON.stringify(s.name)})">Delete</button>
|
||||
onclick="deleteShare('smb',${nameJson})">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).join('');
|
||||
@@ -35,20 +40,22 @@ async function loadShares() {
|
||||
} else {
|
||||
nb.innerHTML = shares.nfs.map(s => {
|
||||
const enabled = s.enabled !== false;
|
||||
const statusDot = `<span class="dot ${enabled ? 'dot-green' : 'dot-red'}" style="margin-right:0"></span>`;
|
||||
const dot = `<span class="dot ${enabled ? 'dot-green' : 'dot-red'}" style="margin-right:0"></span>`;
|
||||
const shareJson = esc(JSON.stringify(s));
|
||||
const pathJson = esc(JSON.stringify(s.path));
|
||||
return `<tr style="${enabled ? '' : 'opacity:.55'}">
|
||||
<td><code>${esc(s.path)}</code></td>
|
||||
<td style="color:var(--muted); font-size:.87em">${esc(s.clients)}</td>
|
||||
<td>${statusDot}</td>
|
||||
<td>${dot}</td>
|
||||
<td>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="openEditModal('nfs',${JSON.stringify(s)})">Edit</button>
|
||||
onclick="openEditModal('nfs',${shareJson})">Edit</button>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="openQuotaModal('nfs',null,${JSON.stringify(s.path)},null)">Quota</button>
|
||||
onclick="openQuotaModal('nfs',null,${pathJson},null)">Quota</button>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="toggleShare('nfs',${JSON.stringify(s.path)},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
||||
onclick="toggleShare('nfs',${pathJson},${!enabled})">${enabled ? 'Disable' : 'Enable'}</button>
|
||||
<button class="btn btn-sm btn-danger"
|
||||
onclick="deleteShare('nfs',${JSON.stringify(s.path)})">Delete</button>
|
||||
onclick="deleteShare('nfs',${pathJson})">Delete</button>
|
||||
</td>
|
||||
</tr>`;
|
||||
}).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 =====
|
||||
|
||||
+28
-15
@@ -7,8 +7,10 @@ async function loadUsers() {
|
||||
tb.innerHTML = `<tr><td colspan="4" style="color:var(--muted)">No users found.</td></tr>`;
|
||||
return;
|
||||
}
|
||||
tb.innerHTML = users.map(u => `
|
||||
<tr>
|
||||
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 `<tr>
|
||||
<td>${esc(u.name)}</td>
|
||||
<td style="color:var(--muted)">${u.uid}</td>
|
||||
<td>
|
||||
@@ -19,11 +21,12 @@ async function loadUsers() {
|
||||
</td>
|
||||
<td>
|
||||
<button class="btn btn-sm me-1" style="border:1px solid var(--border); color:var(--muted)"
|
||||
onclick="openPwModal(${JSON.stringify(u.name)})">Password</button>
|
||||
onclick="openPwModal(${nameJson})">Password</button>
|
||||
<button class="btn btn-sm btn-danger"
|
||||
onclick="deleteUser(${JSON.stringify(u.name)})">Delete</button>
|
||||
onclick="confirmDeleteUser(${nameJson})">Delete</button>
|
||||
</td>
|
||||
</tr>`).join('');
|
||||
</tr>`;
|
||||
}).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'},
|
||||
|
||||
@@ -65,6 +65,25 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Delete confirm modal -->
|
||||
<div class="modal fade" id="deleteModal" tabindex="-1">
|
||||
<div class="modal-dialog modal-sm">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header border-0 pb-0">
|
||||
<h5 class="modal-title" style="color:var(--red)">Delete</h5>
|
||||
<button type="button" class="btn-close btn-close-white" data-bs-dismiss="modal"></button>
|
||||
</div>
|
||||
<div class="modal-body pt-2">
|
||||
<p id="deleteModalMsg" class="mb-0" style="color:var(--muted)"></p>
|
||||
</div>
|
||||
<div class="modal-footer border-0 pt-0">
|
||||
<button type="button" class="btn btn-secondary btn-sm" data-bs-dismiss="modal">Cancel</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" id="deleteModalConfirm">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Header -->
|
||||
<div class="topbar d-flex justify-content-between align-items-center">
|
||||
<span class="fw-semibold fs-5">MicroNAS</span>
|
||||
|
||||
Reference in new issue
Block a user