The server no longer uses CSRF tokens, relying instead on the origin check in `internal/middleware/csrf.go`. Update all documentation, code comments, error handling, and specs to reflect the new protection model and remove the `csrf-secret` configuration key.
93 lines
4.0 KiB
JavaScript
93 lines
4.0 KiB
JavaScript
// SPDX-License-Identifier: AGPL-3.0-only OR LicenseRef-Commercial
|
|
// SPDX-FileCopyrightText: 2025-2026 Christian Galo
|
|
|
|
// Error handling for HTMX requests
|
|
// Shows toast notifications for failed requests and preserves user input.
|
|
//
|
|
// htmx 4 swaps every response except 204/304 by default. Swap suppression is
|
|
// declarative, not scripted: the statuses whose bodies are not designed as
|
|
// swap content (403, 5xx) sit in the "noSwap" list of each page's
|
|
// htmx-config meta tag. 422 swaps natively — its body is the re-rendered
|
|
// form with inline field errors (docs/operator-ux-conventions.md §6), so no
|
|
// script steers that swap anymore. This script only reports:
|
|
// htmx:response:error HTTP >= 400 — toast by status; 422 stays silent
|
|
// htmx:error network failure or timeout — toast
|
|
|
|
// Signal that a request's swap was suppressed or never landed (CSRF expiry,
|
|
// 5xx, network error). sortable-reorder.js listens for this to recover a
|
|
// frozen drag-to-reorder: SortableJS has already moved the DOM row and
|
|
// disabled the instance, so a suppressed swap would leave the table showing
|
|
// the new order with stale rank badges and drag permanently dead. The
|
|
// listener re-enables the instance and re-fetches server order (audit
|
|
// finding #55). It is a no-op unless a reorder was actually in flight, so
|
|
// dispatching broadly is safe.
|
|
function signalReorderRecover() {
|
|
document.body.dispatchEvent(new CustomEvent('operator:reorder-recover'));
|
|
}
|
|
|
|
// Show error toast with a message
|
|
function showErrorToast(message) {
|
|
const toastBody = document.getElementById('errorToastBody');
|
|
const toastEl = document.getElementById('errorToast');
|
|
if (!toastBody || !toastEl) {
|
|
console.error('Error toast elements not found');
|
|
alert(message); // Fallback to alert
|
|
return;
|
|
}
|
|
toastBody.textContent = message;
|
|
const toast = new bootstrap.Toast(toastEl, { delay: 8000 });
|
|
toast.show();
|
|
}
|
|
|
|
// HTTP error responses (status >= 400). Whether the body swaps is decided by
|
|
// the noSwap config, not here.
|
|
document.body.addEventListener('htmx:response:error', function(evt) {
|
|
const ctx = evt.detail && evt.detail.ctx;
|
|
const status = ctx && ctx.response ? ctx.response.status : 0;
|
|
|
|
// 422 Unprocessable Entity is the server-side validation contract: the
|
|
// body carries the form re-rendered with `is-invalid` + `.invalid-feedback`
|
|
// on the offending field(s) and swaps inline by default behavior. The
|
|
// inline message is strictly more useful than a generic toast, so stay
|
|
// silent.
|
|
if (status === 422) {
|
|
return;
|
|
}
|
|
|
|
// 403 Forbidden (in noSwap: the swap is suppressed, user input preserved).
|
|
if (status === 403) {
|
|
const text = (ctx && ctx.text) || '';
|
|
if (text.includes('CSRF') || text.includes('csrf')) {
|
|
// The server's origin check refused the request as cross-origin
|
|
// (internal/middleware/csrf.go): no token is involved, so this is a
|
|
// request whose Sec-Fetch-Site or Origin did not match the site, or
|
|
// a deployment whose base-url is not the origin it is served from.
|
|
// The session itself is fine; a session that has ended arrives as a
|
|
// 401 with HX-Redirect and never reaches here.
|
|
showErrorToast('Refused: the request did not come from this site.');
|
|
} else {
|
|
showErrorToast('Access denied. Please refresh the page and try again.');
|
|
}
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// 5xx server errors (in noSwap: the swap is suppressed).
|
|
if (status >= 500) {
|
|
showErrorToast('Server error. Please try again later.');
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// Other 4xx swap by default — the server may return helpful error HTML —
|
|
// but show a toast as backup.
|
|
showErrorToast('Request failed. Please check your input and try again.');
|
|
});
|
|
|
|
// Request-level failures: network errors, timeouts (htmx 4 defaults to 60s).
|
|
document.body.addEventListener('htmx:error', function(evt) {
|
|
showErrorToast('Network error. Please check your connection and try again.');
|
|
// No swap ever lands, so a drag-to-reorder started here stays frozen too.
|
|
signalReorderRecover();
|
|
});
|