fix: use $.cjax() (Zoraxy CSRF-safe AJAX wrapper) instead of raw fetch()
This commit is contained in:
parent
76e7fc3d5c
commit
4f48790a5a
2 changed files with 66 additions and 93 deletions
136
web/app.js
136
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 || '' }
|
||||
});
|
||||
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();
|
||||
} 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) {
|
||||
try {
|
||||
await apiFetch('/unpin', {
|
||||
method: 'POST',
|
||||
body: { mac: mac, ip: ip, hostname: hostname || '' }
|
||||
});
|
||||
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();
|
||||
} 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');
|
||||
btn.disabled = true;
|
||||
btn.textContent = 'Reloading...';
|
||||
try {
|
||||
await apiFetch('/reload', { method: 'POST' });
|
||||
$.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();
|
||||
} catch (err) {
|
||||
showToast('Reload failed: ' + err.message, 'error');
|
||||
} finally {
|
||||
},
|
||||
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';
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
|
|
|
|||
|
|
@ -41,7 +41,6 @@
|
|||
|
||||
<div id="toast" class="toast hidden"></div>
|
||||
|
||||
<meta name="csrf-token" content="{{.csrfToken}}">
|
||||
<script src="./app.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
|
|||
Loading…
Reference in a new issue