fix: use $.cjax() (Zoraxy CSRF-safe AJAX wrapper) instead of raw fetch()

This commit is contained in:
Claus Lohmar 2026-07-24 11:09:00 +00:00
parent 76e7fc3d5c
commit 4f48790a5a
2 changed files with 66 additions and 93 deletions

View file

@ -1,69 +1,21 @@
/** /**
* DHCP Lease Manager client-side logic. * 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 // Helpers
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
function showToast(message, type) { function showToast(message, type) {
const toast = document.getElementById('toast'); var toast = document.getElementById('toast');
toast.textContent = message; toast.textContent = message;
toast.className = 'toast ' + type; toast.className = 'toast ' + (type || '');
void toast.offsetWidth; // force reflow for transition void toast.offsetWidth;
toast.classList.remove('hidden'); toast.classList.remove('hidden');
setTimeout(function () { toast.classList.add('hidden'); }, 3000); 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 // Render
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
@ -116,58 +68,80 @@ function escAttr(s) {
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
// Actions // Actions (using $.cjax = Zoraxy's CSRF-safe AJAX wrapper)
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
async function loadLeases() { function loadLeases() {
try { $.get('./api/leases', function (data) {
var data = await apiFetch('/leases');
renderLeases(data); renderLeases(data);
} catch (err) { }).fail(function () {
showToast('Failed to load leases: ' + err.message, 'error'); showToast('Failed to load leases', 'error');
} });
} }
async function pinLease(mac, ip, hostname) { function pinLease(mac, ip, hostname) {
try { $.cjax({
await apiFetch('/pin', { url: './api/pin',
method: 'POST', type: 'POST',
body: { mac: mac, ip: ip, hostname: hostname || '' } dataType: 'json',
}); data: { mac: mac, ip: ip, hostname: hostname || '' },
success: function () {
showToast('Pinned ' + (hostname || mac), 'success'); showToast('Pinned ' + (hostname || mac), 'success');
loadLeases(); loadLeases();
} catch (err) { },
showToast('Pin failed: ' + err.message, 'error'); 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) { function unpinLease(mac, ip, hostname) {
try { $.cjax({
await apiFetch('/unpin', { url: './api/unpin',
method: 'POST', type: 'POST',
body: { mac: mac, ip: ip, hostname: hostname || '' } dataType: 'json',
}); data: { mac: mac, ip: ip, hostname: hostname || '' },
success: function () {
showToast('Unpinned ' + (hostname || mac), 'success'); showToast('Unpinned ' + (hostname || mac), 'success');
loadLeases(); loadLeases();
} catch (err) { },
showToast('Unpin failed: ' + err.message, 'error'); 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'); var btn = document.getElementById('reload-btn');
btn.disabled = true; btn.disabled = true;
btn.textContent = 'Reloading...'; btn.textContent = 'Reloading...';
try { $.cjax({
await apiFetch('/reload', { method: 'POST' }); url: './api/reload',
type: 'POST',
dataType: 'json',
data: {},
success: function (data) {
if (data.success) {
showToast('dnsmasq reloaded', 'success'); showToast('dnsmasq reloaded', 'success');
} else {
showToast(data.message || 'Reload failed', 'error');
}
loadLeases(); loadLeases();
} catch (err) { },
showToast('Reload failed: ' + err.message, 'error'); error: function (xhr) {
} finally { 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.disabled = false;
btn.textContent = 'Reload dnsmasq'; btn.textContent = 'Reload dnsmasq';
} }
});
} }
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------

View file

@ -41,7 +41,6 @@
<div id="toast" class="toast hidden"></div> <div id="toast" class="toast hidden"></div>
<meta name="csrf-token" content="{{.csrfToken}}">
<script src="./app.js"></script> <script src="./app.js"></script>
</body> </body>
</html> </html>