diff --git a/web/app.js b/web/app.js index 35c09b2..d4d453c 100644 --- a/web/app.js +++ b/web/app.js @@ -1,69 +1,21 @@ /** * DHCP Lease Manager — client-side logic. - * All API URLs are relative to the page path, so the Zoraxy proxy handles routing. + * Uses Zoraxy's $.cjax() for CSRF-safe AJAX (jQuery is available from parent frame). */ -const API_BASE = './api'; -const csrfToken = document.querySelector('meta[name="csrf-token"]')?.content || ''; - // --------------------------------------------------------------------------- // Helpers // --------------------------------------------------------------------------- function showToast(message, type) { - const toast = document.getElementById('toast'); + var toast = document.getElementById('toast'); toast.textContent = message; - toast.className = 'toast ' + type; - void toast.offsetWidth; // force reflow for transition + toast.className = 'toast ' + (type || ''); + void toast.offsetWidth; toast.classList.remove('hidden'); setTimeout(function () { toast.classList.add('hidden'); }, 3000); } -/** Minimal fetch wrapper that includes the CSRF token. */ -async function apiFetch(path, opts) { - opts = opts || {}; - opts.headers = opts.headers || {}; - var method = (opts.method || 'GET').toUpperCase(); - var isPost = (method === 'POST' || method === 'PUT' || method === 'PATCH'); - - // Attach CSRF token as query param (Zoraxy may validate it there) - if (csrfToken && csrfToken !== '{{.csrfToken}}') { - var sep = path.indexOf('?') >= 0 ? '&' : '?'; - path = path + sep + 'csrfToken=' + encodeURIComponent(csrfToken); - opts.headers['X-Zoraxy-Csrf'] = csrfToken; - } - - if (isPost && opts.body && typeof opts.body === 'object') { - // Send as URL-encoded form data (Zoraxy's CSRF middleware parses forms, not JSON) - var formBody = new URLSearchParams(); - for (var key in opts.body) { - if (opts.body.hasOwnProperty(key)) { - formBody.append(key, opts.body[key]); - } - } - formBody.append('csrfToken', csrfToken || ''); - opts.body = formBody.toString(); - opts.headers['Content-Type'] = 'application/x-www-form-urlencoded'; - } - var res = await fetch(API_BASE + path, opts); - var text = await res.text(); - try { - var data = JSON.parse(text); - } catch (e) { - throw new Error('Unexpected response (status ' + res.status + '). Check plugin is running.'); - } - if (!res.ok) { - throw new Error(data.error || data.message || 'Request failed (status ' + res.status + ')'); - } - return data; -} - -function formatTime(t) { - var d = new Date(t); - if (isNaN(d.getTime())) return '--'; - return d.toLocaleString(); -} - // --------------------------------------------------------------------------- // Render // --------------------------------------------------------------------------- @@ -116,58 +68,80 @@ function escAttr(s) { } // --------------------------------------------------------------------------- -// Actions +// Actions (using $.cjax = Zoraxy's CSRF-safe AJAX wrapper) // --------------------------------------------------------------------------- -async function loadLeases() { - try { - var data = await apiFetch('/leases'); +function loadLeases() { + $.get('./api/leases', function (data) { renderLeases(data); - } catch (err) { - showToast('Failed to load leases: ' + err.message, 'error'); - } + }).fail(function () { + showToast('Failed to load leases', 'error'); + }); } -async function pinLease(mac, ip, hostname) { - try { - await apiFetch('/pin', { - method: 'POST', - body: { mac: mac, ip: ip, hostname: hostname || '' } - }); - showToast('Pinned ' + (hostname || mac), 'success'); - loadLeases(); - } catch (err) { - showToast('Pin failed: ' + err.message, 'error'); - } +function pinLease(mac, ip, hostname) { + $.cjax({ + url: './api/pin', + type: 'POST', + dataType: 'json', + data: { mac: mac, ip: ip, hostname: hostname || '' }, + success: function () { + showToast('Pinned ' + (hostname || mac), 'success'); + loadLeases(); + }, + error: function (xhr) { + var msg = 'Pin failed'; + try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {} + showToast(msg, 'error'); + } + }); } -async function unpinLease(mac, ip, hostname) { - try { - await apiFetch('/unpin', { - method: 'POST', - body: { mac: mac, ip: ip, hostname: hostname || '' } - }); - showToast('Unpinned ' + (hostname || mac), 'success'); - loadLeases(); - } catch (err) { - showToast('Unpin failed: ' + err.message, 'error'); - } +function unpinLease(mac, ip, hostname) { + $.cjax({ + url: './api/unpin', + type: 'POST', + dataType: 'json', + data: { mac: mac, ip: ip, hostname: hostname || '' }, + success: function () { + showToast('Unpinned ' + (hostname || mac), 'success'); + loadLeases(); + }, + error: function (xhr) { + var msg = 'Unpin failed'; + try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {} + showToast(msg, 'error'); + } + }); } -async function reloadDnsmasq() { +function reloadDnsmasq() { var btn = document.getElementById('reload-btn'); btn.disabled = true; btn.textContent = 'Reloading...'; - try { - await apiFetch('/reload', { method: 'POST' }); - showToast('dnsmasq reloaded', 'success'); - loadLeases(); - } catch (err) { - showToast('Reload failed: ' + err.message, 'error'); - } finally { - btn.disabled = false; - btn.textContent = 'Reload dnsmasq'; - } + $.cjax({ + url: './api/reload', + type: 'POST', + dataType: 'json', + data: {}, + success: function (data) { + if (data.success) { + showToast('dnsmasq reloaded', 'success'); + } else { + showToast(data.message || 'Reload failed', 'error'); + } + loadLeases(); + }, + error: function (xhr) { + var msg = 'Reload failed'; + try { var r = JSON.parse(xhr.responseText); msg = r.message || r.error || msg; } catch(e) {} + showToast(msg, 'error'); + }, + complete: function () { + btn.disabled = false; + btn.textContent = 'Reload dnsmasq'; + } + }); } // --------------------------------------------------------------------------- diff --git a/web/index.html b/web/index.html index cd1e815..758f801 100644 --- a/web/index.html +++ b/web/index.html @@ -41,7 +41,6 @@
-