diff --git a/app.js b/app.js index f048518..cc9c087 100644 --- a/app.js +++ b/app.js @@ -153,12 +153,16 @@ async function shareFile(blob, filename, mime, titleHint) { /* ----- Auth-Check ----- */ async function ensureAuth() { - const t = await api.getToken(); - if (!t) { - router.go('#/login'); - return false; + // Session steckt im awl_sso-Cookie -> serverseitig prüfen (sliding renewal). + try { + await api.request('/verify.php'); + return true; + } catch (e) { + // 401 -> Cookie fehlt/abgelaufen -> Login. Netzfehler (offline) -> NICHT + // ausloggen, die PWA muss offline nutzbar bleiben. + if (e.status === 401) { router.go('#/login'); return false; } + return true; } - return true; } /* ============================================================ @@ -298,15 +302,8 @@ async function promptNewPin() { * Startet die App — fragt ggf. PIN ab bevor router läuft. */ window.appBoot = async function appBoot() { - // JWT aktiv aus IndexedDB preloaden, bevor eine Route rennt. - // Verhindert Race-Conditions, in denen ensureAuth() zu früh ein `null` bekommt - // und nach Login-Screen redirectet, obwohl ein gültiges Token in IDB liegt. - try { - const t = await api.getToken(); - console.log('[boot] jwt vorhanden:', !!t); - } catch (e) { - console.warn('[boot] Token-Preload fehlgeschlagen', e); - } + // Kein Token-Preload mehr — die Session steckt im HttpOnly-Cookie awl_sso, das + // der Browser bei jedem same-origin-Request automatisch mitschickt. // Bootstrap-Puffer: legt einen zusätzlichen History-Eintrag an, damit beim // ersten Android-Back der popstate-Handler greifen kann (Toast „Nochmal @@ -742,9 +739,8 @@ router.on('/orders/:id', async (args) => { } btn.textContent = '⏳'; try { - const t = await api.getToken(); - const params = new URLSearchParams({ relpath: rel, jwt: t }); - const r = await fetch(window.location.origin + '/custom/bericht/api/photo.php?' + params.toString()); + const params = new URLSearchParams({ relpath: rel }); + const r = await fetch(window.location.origin + '/custom/bericht/api/photo.php?' + params.toString(), { credentials: 'same-origin' }); if (!r.ok) throw new Error('Load failed'); const blob = await r.blob(); const url = URL.createObjectURL(new Blob([blob], { type: mime })); @@ -2233,12 +2229,12 @@ function openShipmentSignatureModal(shipmentId, info) { // Direkter Fetch statt api.confirmShipment, damit wir den Response-Body // auch bei 500 lesen koennen - const token = await api.getToken(); const url = window.location.origin + '/custom/bericht/api/shipments.php?id=' + shipmentId + '&action=confirm'; const r = await fetch(url, { method: 'POST', body: fd, - headers: { 'Authorization': 'Bearer ' + token }, + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, // State-Änderung -> CSRF-pflichtig }); const text = await r.text(); let payload; @@ -2516,8 +2512,7 @@ async function openPdfViewer(blob, filename, relpath) { modal.querySelector('#pdf-prev').onclick = () => renderPage(currentPage - 1); modal.querySelector('#pdf-next').onclick = () => renderPage(currentPage + 1); modal.querySelector('#pdf-download').onclick = async () => { - const t = await api.getToken(); - const params = new URLSearchParams({ relpath, jwt: t, download: 1 }); + const params = new URLSearchParams({ relpath, download: 1 }); window.location.href = window.location.origin + '/custom/bericht/api/photo.php?' + params.toString(); }; modal.querySelector('#pdf-share').onclick = () => { diff --git a/lib/api.js b/lib/api.js index a65405f..11b2026 100644 --- a/lib/api.js +++ b/lib/api.js @@ -24,9 +24,10 @@ } async function request(path, opts = {}) { - const t = await getToken(); const headers = opts.headers || {}; - if (t && !headers.Authorization) headers.Authorization = 'Bearer ' + t; + // Same-origin-CSRF-Schutz von awlauth verlangt diesen Header. Vanilla fetch + // setzt ihn — anders als jQuery — NICHT automatisch; ohne ihn: 403. + if (!headers['X-Requested-With']) headers['X-Requested-With'] = 'XMLHttpRequest'; if (!headers['Content-Type'] && !(opts.body instanceof FormData)) { headers['Content-Type'] = 'application/json'; } @@ -41,7 +42,7 @@ } let r; try { - r = await fetch(API_BASE + path, { ...opts, headers, signal }); + r = await fetch(API_BASE + path, { ...opts, headers, signal, credentials: 'same-origin' }); } finally { if (toHandle) clearTimeout(toHandle); } @@ -72,12 +73,14 @@ method: 'POST', body: JSON.stringify({ login: loginName, password }), }); - await setToken(r.token); + // Kein Token mehr im Body — die Session steckt im HttpOnly-Cookie awl_sso. await idb.set('user', r.user); return r; } async function logout() { + // Server-Logout: löscht das awl_sso-Cookie (Single-Logout über alle AWL-Apps). + try { await request('/logout.php', { method: 'POST' }); } catch (_) {} await clearToken(); } @@ -242,10 +245,13 @@ } async function getShipmentPdfBlobUrl(shipmentId) { - const t = await getToken(); - if (!t) return null; - const params = new URLSearchParams({ id: shipmentId, action: 'pdf', jwt: t }); - const r = await fetch(API_BASE + '/shipments.php?' + params.toString()); + const params = new URLSearchParams({ id: shipmentId, action: 'pdf' }); + // shipments.php nutzt awlauth_require (CSRF) -> X-Requested-With nötig. + // Per fetch setzbar (kein window.location für das Shipment-PDF). + const r = await fetch(API_BASE + '/shipments.php?' + params.toString(), { + credentials: 'same-origin', + headers: { 'X-Requested-With': 'XMLHttpRequest' }, + }); if (!r.ok) return null; const blob = await r.blob(); return { url: URL.createObjectURL(blob), blob }; @@ -271,31 +277,27 @@ } async function getPdfBlobUrl(berichtId) { - const t = await getToken(); - if (!t) return null; - const params = new URLSearchParams({ id: berichtId, jwt: t }); - const r = await fetch(API_BASE + '/pdf.php?' + params.toString()); + // pdf.php nutzt awlauth_verify (kein CSRF) — Cookie reicht. + const params = new URLSearchParams({ id: berichtId }); + const r = await fetch(API_BASE + '/pdf.php?' + params.toString(), { credentials: 'same-origin' }); if (!r.ok) return null; const blob = await r.blob(); return URL.createObjectURL(blob); } /** - * Lädt eine Bild-Datei von der API als Blob-URL (inkl. JWT). - * Wird benötigt weil keine Authorization-Header mitschickt. + * Lädt eine Bild-Datei von der API als Blob-URL. + * Auth über das awl_sso-Cookie (same-origin) — photo.php nutzt awlauth_verify. */ const blobUrlCache = new Map(); async function getPhotoBlobUrl(relpath, size) { const key = (size || 'full') + '|' + relpath; if (blobUrlCache.has(key)) return blobUrlCache.get(key); - const t = await getToken(); - if (!t) return null; - // JWT als Query-Param, weil Apache auf prod den Authorization-Header filtert - const params = new URLSearchParams({ relpath, jwt: t }); + const params = new URLSearchParams({ relpath }); if (size) params.set('size', size); - const r = await fetch(API_BASE + '/photo.php?' + params.toString()); + const r = await fetch(API_BASE + '/photo.php?' + params.toString(), { credentials: 'same-origin' }); if (!r.ok) { const body = await r.text().catch(() => ''); console.warn('photo.php failed', r.status, relpath, body); @@ -318,15 +320,13 @@ blobUrlCache.clear(); } - // Lädt eine beliebige Datei (PDF, DOCX, ...) als Blob-URL inkl. JWT. + // Lädt eine beliebige Datei (PDF, DOCX, ...) als Blob-URL. Auth per awl_sso-Cookie. // Ohne Mime-Filter — Aufrufer entscheidet selbst, was damit passiert. async function getFileBlobUrl(relpath) { - const t = await getToken(); - if (!t) return null; - const params = new URLSearchParams({ relpath, jwt: t }); + const params = new URLSearchParams({ relpath }); const url = API_BASE + '/photo.php?' + params.toString(); console.log('[API] Fetching file:', relpath); - const r = await fetch(url); + const r = await fetch(url, { credentials: 'same-origin' }); if (!r.ok) { console.warn('[API] File fetch failed', r.status, relpath); return null;