diff --git a/website/js/api.js b/website/js/api.js index 9d72ea5..86d97e8 100644 --- a/website/js/api.js +++ b/website/js/api.js @@ -1,4 +1,29 @@ -const API_BASE = '../api'; +/** + * Resolve API base for sibling (/website + /api) or subpath (/prefix/ + /prefix/api) layouts. + * Override per deploy: + */ +function resolveApiBase() { + if (typeof window !== 'undefined' && window.QDB_API_BASE) { + return String(window.QDB_API_BASE).replace(/\/$/, ''); + } + const fromRelative = new URL('../api', window.location.href); + const relPath = fromRelative.pathname.replace(/\/$/, ''); + const pageDir = window.location.pathname.replace(/\/[^/]*$/, '/') || '/'; + const mount = pageDir.split('/').filter(Boolean)[0]; + // Subpath deploy: /prefix/ is aliased to website/, so ../api wrongly becomes /api + if (mount && mount !== 'api' && mount !== 'website' && relPath === '/api') { + return `/${mount}/api`; + } + return relPath; +} + +const API_BASE = resolveApiBase(); + +/** Build a URL under the API base (path may include query string). */ +export function apiUrl(endpoint) { + const clean = String(endpoint).replace(/^\//, '').replace(/\.php(\?|$)/, '$1'); + return `${API_BASE}/${clean}`; +} function getToken() { return localStorage.getItem('qdb_token'); @@ -107,7 +132,7 @@ async function apiFetch(endpoint, options = {}) { // Strip .php suffix for clean URLs const cleanEndpoint = endpoint.replace(/\.php(\?|$)/, '$1'); - const res = await fetch(`${API_BASE}/${cleanEndpoint}`, { ...options, headers }); + const res = await fetch(apiUrl(cleanEndpoint), { ...options, headers }); if (res.status === 401 || res.status === 403) { await handleAuthResponse(res); diff --git a/website/js/pages/dev.js b/website/js/pages/dev.js index 5f8c7f7..a29efea 100644 --- a/website/js/pages/dev.js +++ b/website/js/pages/dev.js @@ -1,4 +1,4 @@ -import { apiGet, apiPost, apiPut, apiDownloadFetch, redirectToLogin } from '../api.js'; +import { apiGet, apiPost, apiPut, apiDownloadFetch, redirectToLogin, apiUrl } from '../api.js'; import { getRole, pageHeaderHTML, showToast } from '../app.js'; import { confirmAction } from '../confirm-modal.js'; import { navigate } from '../router.js'; @@ -494,7 +494,7 @@ function wireAdminZipExport() { const prev = btn.textContent; btn.textContent = 'Preparing ZIP…'; try { - await downloadExportZip('../api/export?exportAll=1', 'responses_export_current.zip'); + await downloadExportZip(apiUrl('export?exportAll=1'), 'responses_export_current.zip'); showToast('ZIP download started (current data)', 'success'); } catch (e) { showToast(e.message, 'error'); @@ -510,7 +510,7 @@ function wireAdminZipExport() { const prev = btn.textContent; btn.textContent = 'Preparing ZIP…'; try { - await downloadExportZip('../api/export?exportAll=1&allVersions=1', 'responses_export_all_versions.zip'); + await downloadExportZip(apiUrl('export?exportAll=1&allVersions=1'), 'responses_export_all_versions.zip'); showToast('ZIP download started (all versions)', 'success'); } catch (e) { showToast(e.message, 'error'); diff --git a/website/js/pages/export.js b/website/js/pages/export.js index 41ee675..314f8d9 100644 --- a/website/js/pages/export.js +++ b/website/js/pages/export.js @@ -1,4 +1,4 @@ -import { apiGet, apiDownloadFetch } from '../api.js'; +import { apiGet, apiDownloadFetch, apiUrl } from '../api.js'; import { canEdit, pageHeaderHTML, showToast } from '../app.js'; let questionnairesList = []; let filterSearch = ''; @@ -153,7 +153,7 @@ function wireExportActions() { const prev = btn.textContent; btn.textContent = 'Preparing…'; try { - const res = await apiDownloadFetch('../api/export?bundle=1'); + const res = await apiDownloadFetch(apiUrl('export?bundle=1')); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || err.error || 'Export failed'); @@ -197,7 +197,7 @@ async function onExportCsvClick(ev) { const prev = btn.textContent; btn.textContent = 'Downloading…'; try { - const url = `../api/export?questionnaireID=${encodeURIComponent(btn.dataset.id)}`; + const url = apiUrl(`export?questionnaireID=${encodeURIComponent(btn.dataset.id)}`); await downloadExportCsv(btn, url, `${btn.dataset.name}_v${btn.dataset.version}.csv`); showToast('Download started', 'success'); } catch (e) { @@ -214,7 +214,7 @@ async function onExportAllVersionsClick(ev) { const prev = btn.textContent; btn.textContent = 'Downloading…'; try { - const url = `../api/export?questionnaireID=${encodeURIComponent(btn.dataset.id)}&allVersions=1`; + const url = apiUrl(`export?questionnaireID=${encodeURIComponent(btn.dataset.id)}&allVersions=1`); const stamp = new Date().toISOString().slice(0, 10); await downloadExportCsv(btn, url, `${btn.dataset.name}_all_versions_${stamp}.csv`); showToast('All versions download started', 'success'); diff --git a/website/js/pages/login.js b/website/js/pages/login.js index 0197e1d..361d0da 100644 --- a/website/js/pages/login.js +++ b/website/js/pages/login.js @@ -1,6 +1,6 @@ import { navigate } from '../router.js'; import { isLoggedIn, showToast } from '../app.js'; -import { apiGet, invalidateSessionCheck } from '../api.js'; +import { apiGet, invalidateSessionCheck, apiUrl } from '../api.js'; const WEBSITE_ROLES = ['admin', 'supervisor']; @@ -103,7 +103,7 @@ export async function loginPage() { setFormBusy('loginForm', true, 'Signing in…'); try { - const res = await fetch('../api/auth/login', { + const res = await fetch(apiUrl('auth/login'), { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -176,7 +176,7 @@ export async function loginPage() { if (pendingTempToken) { headers['Authorization'] = `Bearer ${pendingTempToken}`; } - const res = await fetch('../api/auth/change-password', { + const res = await fetch(apiUrl('auth/change-password'), { method: 'POST', headers, body: JSON.stringify({ username: pendingUsername, old_password: oldPw, new_password: newPw }), diff --git a/website/js/pages/translations.js b/website/js/pages/translations.js index 61a6ec6..1103a5e 100644 --- a/website/js/pages/translations.js +++ b/website/js/pages/translations.js @@ -1,4 +1,4 @@ -import { apiGet, apiPost, apiPut, apiDelete, apiDownloadFetch } from '../api.js'; +import { apiGet, apiPost, apiPut, apiDelete, apiDownloadFetch, apiUrl } from '../api.js'; import { canEdit, pageHeaderHTML, showToast } from '../app.js'; import { confirmAction } from '../confirm-modal.js'; import { @@ -634,7 +634,7 @@ function bindTranslationBundleActions() { const prev = exportBtn.textContent; exportBtn.textContent = 'Preparing…'; try { - const res = await apiDownloadFetch('../api/translations?exportBundle=1'); + const res = await apiDownloadFetch(apiUrl('translations?exportBundle=1')); if (!res.ok) { const err = await res.json().catch(() => ({})); throw new Error(err.error?.message || err.error || 'Export failed');