Fix HTMX expired-session handling, CSP-blocked form behaviors, reorder recovery, billing currency display, plan/checkout guards, FedWiki quota edge cases, and operator/member empty/error states. Add entitlement uniqueness migrations, canonical migration source wiring, and regression coverage for the remediated flows. Update status docs with the audit triage and model inventory.
104 lines
3.9 KiB
JavaScript
104 lines
3.9 KiB
JavaScript
// Error handling for HTMX requests
|
|
// Shows toast notifications for failed requests and preserves user input
|
|
|
|
// Signal that a request's swap was suppressed (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();
|
|
}
|
|
|
|
// Handle HTMX response errors - this fires BEFORE swap
|
|
document.body.addEventListener('htmx:beforeSwap', function(evt) {
|
|
const xhr = evt.detail.xhr;
|
|
const status = xhr.status;
|
|
|
|
// Handle 403 Forbidden (CSRF errors)
|
|
if (status === 403) {
|
|
// Check if it's a CSRF error by looking at response
|
|
const responseText = xhr.responseText || '';
|
|
if (responseText.includes('CSRF') || responseText.includes('csrf')) {
|
|
// Prevent the swap so user input is preserved
|
|
evt.detail.shouldSwap = false;
|
|
evt.detail.isError = false; // Prevent htmx:responseError from firing
|
|
|
|
showErrorToast('Your session has expired. Please refresh the page to continue.');
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
// Other 403 errors
|
|
evt.detail.shouldSwap = false;
|
|
showErrorToast('Access denied. Please refresh the page and try again.');
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// Handle 5xx server errors
|
|
if (status >= 500) {
|
|
evt.detail.shouldSwap = false;
|
|
showErrorToast('Server error. Please try again later.');
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// Handle network errors (status 0)
|
|
if (status === 0) {
|
|
evt.detail.shouldSwap = false;
|
|
showErrorToast('Network error. Please check your connection and try again.');
|
|
signalReorderRecover();
|
|
return;
|
|
}
|
|
|
|
// 422 Unprocessable Entity is the server-side validation contract per
|
|
// docs/operator-ux-conventions.md §6: the response body carries the form
|
|
// re-rendered with `is-invalid` + `.invalid-feedback` on the offending
|
|
// field(s). htmx 2.x's default responseHandling has `[45]..` set to
|
|
// `swap:false`, so we must explicitly enable the swap (and clear the
|
|
// error flag) for the validation body to land in the DOM. The inline
|
|
// message is strictly more useful than a generic "check your input"
|
|
// toast, so stay silent on the toast side.
|
|
if (status === 422) {
|
|
evt.detail.shouldSwap = true;
|
|
evt.detail.isError = false;
|
|
return;
|
|
}
|
|
|
|
// For other 4xx errors, allow the swap as the server may return helpful error HTML
|
|
// but show a toast as backup
|
|
if (status >= 400 && status < 500) {
|
|
evt.detail.shouldSwap = true;
|
|
showErrorToast('Request failed. Please check your input and try again.');
|
|
}
|
|
});
|
|
|
|
// Fallback handler for any errors that slip through
|
|
document.body.addEventListener('htmx:responseError', function(evt) {
|
|
console.error('HTMX Response Error:', evt.detail);
|
|
// Toast already shown by beforeSwap handler in most cases
|
|
});
|
|
|
|
// Handle send errors (network failures before response)
|
|
document.body.addEventListener('htmx:sendError', function(evt) {
|
|
showErrorToast('Failed to send request. Please check your connection.');
|
|
// No swap ever lands, so a drag-to-reorder started here stays frozen too.
|
|
signalReorderRecover();
|
|
});
|