feat(js): harden global HTTP and module import contracts
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
* Supports slide-out animation and hover-pause.
|
||||
*/
|
||||
import { resolveHost } from '../core/app-dom.js';
|
||||
import { postForm as postFormRequest } from '../core/app-http.js';
|
||||
|
||||
export function initFlashAutoDismiss(root = document, options = {}) {
|
||||
const {
|
||||
@@ -32,17 +33,14 @@ export function initFlashAutoDismiss(root = document, options = {}) {
|
||||
const postForm = async (form) => {
|
||||
const action = form.getAttribute('action');
|
||||
if (!action) {
|
||||
return null;
|
||||
return false;
|
||||
}
|
||||
try {
|
||||
await postFormRequest(action, new FormData(form));
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
const body = new URLSearchParams(new FormData(form));
|
||||
return fetch(action, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'X-Requested-With': 'fetch',
|
||||
},
|
||||
body,
|
||||
});
|
||||
};
|
||||
|
||||
notices.forEach((notice) => {
|
||||
@@ -62,8 +60,8 @@ export function initFlashAutoDismiss(root = document, options = {}) {
|
||||
}
|
||||
const form = notice.querySelector('form');
|
||||
if (form) {
|
||||
const response = await postForm(form);
|
||||
if (!response || !response.ok) {
|
||||
const ok = await postForm(form);
|
||||
if (!ok) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
@@ -80,8 +78,8 @@ export function initFlashAutoDismiss(root = document, options = {}) {
|
||||
if (destroyed) return;
|
||||
const form = notice.querySelector('form');
|
||||
if (form) {
|
||||
const response = await postForm(form);
|
||||
if (!response || !response.ok) return;
|
||||
const ok = await postForm(form);
|
||||
if (!ok) return;
|
||||
}
|
||||
dismissNotice(notice);
|
||||
}, 1000);
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
* preview API and renders them grouped by category with keyboard navigation.
|
||||
*/
|
||||
import { warnOnce, resolveHost } from '../core/app-dom.js';
|
||||
import { getJson } from '../core/app-http.js';
|
||||
import { isEditableTarget } from '../core/app-form-utils.js';
|
||||
|
||||
const ICON_MAP = {
|
||||
@@ -221,13 +222,7 @@ export function initGlobalSearch(root = document, options = {}) {
|
||||
url.searchParams.set('q', value);
|
||||
|
||||
try {
|
||||
const response = await fetch(url.toString(), { headers: { 'Accept': 'application/json' }, signal });
|
||||
if (!response.ok) {
|
||||
warnOnce('FETCH_FAILED', `Global search failed: ${response.status} ${response.statusText}`, { module: 'global-search' });
|
||||
setState('error');
|
||||
return;
|
||||
}
|
||||
const data = await response.json();
|
||||
const data = await getJson(url.toString(), { signal });
|
||||
if (Array.isArray(data)) {
|
||||
render(data, value);
|
||||
} else {
|
||||
@@ -236,7 +231,7 @@ export function initGlobalSearch(root = document, options = {}) {
|
||||
}
|
||||
} catch (err) {
|
||||
if (err.name === 'AbortError') {return;}
|
||||
warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err });
|
||||
warnOnce('FETCH_ERROR', 'Global search fetch error', { module: 'global-search', err, status: err?.status });
|
||||
setState('error');
|
||||
}
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Editor.js integration — lifecycle-managed page editor module.
|
||||
*/
|
||||
import EditorJS from '../../vendor/editorjs/editorjs.mjs';
|
||||
import { postForm } from '../core/app-http.js';
|
||||
import { warnOnce, resolveHost } from '../core/app-dom.js';
|
||||
|
||||
const parseData = (value) => {
|
||||
@@ -194,21 +195,8 @@ export function initPageEditor(root = document, config = {}) {
|
||||
form.dataset.submitting = '1';
|
||||
|
||||
try {
|
||||
const formData = new FormData(form);
|
||||
const response = await fetch(form.action || window.location.pathname, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
Accept: 'application/json',
|
||||
},
|
||||
body: formData,
|
||||
});
|
||||
if (response.redirected) {
|
||||
window.location.href = response.url;
|
||||
return;
|
||||
}
|
||||
const data = await response.json().catch(() => null);
|
||||
if (data && data.redirect) {
|
||||
const data = await postForm(form.action || window.location.pathname, new FormData(form));
|
||||
if (data && typeof data === 'object' && typeof data.redirect === 'string' && data.redirect.trim() !== '') {
|
||||
window.location.href = data.redirect;
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Session expiry warning dialog.
|
||||
*/
|
||||
import { warnOnce, resolveHost } from '../core/app-dom.js';
|
||||
import { postForm } from '../core/app-http.js';
|
||||
|
||||
const WARNING_LEAD_SECONDS = 120;
|
||||
const TICK_INTERVAL_MS = 1000;
|
||||
@@ -211,24 +212,9 @@ export function initSessionWarning(root = document, runtimeConfig = {}) {
|
||||
extendButton.disabled = true;
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
const data = await postForm(config.pingUrl, {
|
||||
[config.csrfKey]: config.csrfToken,
|
||||
});
|
||||
|
||||
const response = await fetch(config.pingUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
window.location.reload();
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (typeof data.idle_timeout_seconds === 'number' && data.idle_timeout_seconds > 0) {
|
||||
config.idleSeconds = data.idle_timeout_seconds;
|
||||
}
|
||||
@@ -267,23 +253,9 @@ export function initSessionWarning(root = document, runtimeConfig = {}) {
|
||||
hasInteractionSinceLastSync = false;
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
const data = await postForm(config.pingUrl, {
|
||||
[config.csrfKey]: config.csrfToken,
|
||||
});
|
||||
|
||||
const response = await fetch(config.pingUrl, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'X-Requested-With': 'XMLHttpRequest',
|
||||
},
|
||||
body,
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
return;
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
if (typeof data.idle_timeout_seconds === 'number' && data.idle_timeout_seconds > 0) {
|
||||
config.idleSeconds = data.idle_timeout_seconds;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
*/
|
||||
import { showAsyncFlash } from './app-async-flash.js';
|
||||
import { warnOnce, resolveHost } from '../core/app-dom.js';
|
||||
import { postForm } from '../core/app-http.js';
|
||||
|
||||
const SWITCHER_ROOT_SELECTOR = '[data-tenant-switcher]';
|
||||
const TENANT_LINK_SELECTOR = '[data-switch-tenant]';
|
||||
@@ -32,40 +33,22 @@ const switchTenant = async (link) => {
|
||||
setPending(link, true);
|
||||
|
||||
try {
|
||||
const body = new URLSearchParams({
|
||||
const data = await postForm('admin/users/switch-tenant', {
|
||||
tenant_id: tenantId,
|
||||
[csrfKey]: csrfToken
|
||||
});
|
||||
|
||||
const response = await fetch('admin/users/switch-tenant', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch'
|
||||
},
|
||||
body
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
const data = await response.json();
|
||||
if (data.ok) {
|
||||
if (data.theme) {
|
||||
document.documentElement.dataset.theme = data.theme;
|
||||
}
|
||||
window.location.reload();
|
||||
} else {
|
||||
warnOnce('FETCH_FAILED', 'Tenant switch failed', { module: 'tenant-switcher', error: data.error });
|
||||
showAsyncFlash('error', errorMessage);
|
||||
setPending(link, false);
|
||||
if (data && data.ok) {
|
||||
if (data.theme) {
|
||||
document.documentElement.dataset.theme = data.theme;
|
||||
}
|
||||
window.location.reload();
|
||||
} else {
|
||||
warnOnce('FETCH_FAILED', `Tenant switch server error: ${response.status}`, { module: 'tenant-switcher' });
|
||||
warnOnce('FETCH_FAILED', 'Tenant switch failed', { module: 'tenant-switcher', error: data?.error ?? '' });
|
||||
showAsyncFlash('error', errorMessage);
|
||||
setPending(link, false);
|
||||
}
|
||||
} catch (error) {
|
||||
warnOnce('FETCH_ERROR', 'Tenant switch network error', { module: 'tenant-switcher', error });
|
||||
warnOnce('FETCH_ERROR', 'Tenant switch request failed', { module: 'tenant-switcher', error, status: error?.status });
|
||||
showAsyncFlash('error', errorMessage);
|
||||
setPending(link, false);
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
* Theme switcher — light/dark toggle and menu with server-side persistence.
|
||||
*/
|
||||
import { warnOnce, resolveHost } from '../core/app-dom.js';
|
||||
import { postForm } from '../core/app-http.js';
|
||||
|
||||
const setTheme = (theme) => {
|
||||
document.documentElement.dataset.theme = theme;
|
||||
@@ -24,17 +25,12 @@ const updateTheme = async (source, theme) => {
|
||||
if (!url || !csrfKey || !csrfToken) {
|
||||
return true;
|
||||
}
|
||||
const body = new URLSearchParams({ theme, [csrfKey]: csrfToken });
|
||||
const response = await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/x-www-form-urlencoded',
|
||||
'Accept': 'application/json',
|
||||
'X-Requested-With': 'fetch',
|
||||
},
|
||||
body,
|
||||
});
|
||||
return response.ok;
|
||||
try {
|
||||
await postForm(url, { theme, [csrfKey]: csrfToken });
|
||||
return true;
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
export function initThemeControls(root = document, options = {}) {
|
||||
|
||||
Reference in New Issue
Block a user