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.
|
* 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';
|
||||||
}
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
|
|
|
||||||
|
|
@ -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>
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue