Baustelle-PWA: SSO-Migration auf awlauth (JWT -> awl_sso-Cookie)
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
All checks were successful
Deploy baustelle-pwa / deploy (push) Successful in 13s
- lib/api.js request(): Authorization:Bearer raus, X-Requested-With + credentials:same-origin rein (Vanilla fetch setzt X-Requested-With nicht automatisch -> ohne ihn 403 an awlauth_require). - login(): kein setToken mehr (Session im HttpOnly-Cookie); logout() ruft jetzt /logout.php (Single-Logout). - Blob-Loader (photo/pdf/file/shipment-pdf): jwt-Query raus, credentials rein; Shipment-PDF setzt zusaetzlich X-Requested-With (shipments.php=CSRF). - app.js: ensureAuth() prueft /verify.php (offline nicht ausloggen), Token-Preload raus, confirm-POST mit X-Requested-With, Audio-/PDF-Download ohne jwt-Query. [deploy]
This commit is contained in:
parent
3608fef216
commit
17575f38e4
2 changed files with 40 additions and 45 deletions
37
app.js
37
app.js
|
|
@ -153,12 +153,16 @@ async function shareFile(blob, filename, mime, titleHint) {
|
||||||
|
|
||||||
/* ----- Auth-Check ----- */
|
/* ----- Auth-Check ----- */
|
||||||
async function ensureAuth() {
|
async function ensureAuth() {
|
||||||
const t = await api.getToken();
|
// Session steckt im awl_sso-Cookie -> serverseitig prüfen (sliding renewal).
|
||||||
if (!t) {
|
try {
|
||||||
router.go('#/login');
|
await api.request('/verify.php');
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/* ============================================================
|
/* ============================================================
|
||||||
|
|
@ -298,15 +302,8 @@ async function promptNewPin() {
|
||||||
* Startet die App — fragt ggf. PIN ab bevor router läuft.
|
* Startet die App — fragt ggf. PIN ab bevor router läuft.
|
||||||
*/
|
*/
|
||||||
window.appBoot = async function appBoot() {
|
window.appBoot = async function appBoot() {
|
||||||
// JWT aktiv aus IndexedDB preloaden, bevor eine Route rennt.
|
// Kein Token-Preload mehr — die Session steckt im HttpOnly-Cookie awl_sso, das
|
||||||
// Verhindert Race-Conditions, in denen ensureAuth() zu früh ein `null` bekommt
|
// der Browser bei jedem same-origin-Request automatisch mitschickt.
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Bootstrap-Puffer: legt einen zusätzlichen History-Eintrag an, damit beim
|
// Bootstrap-Puffer: legt einen zusätzlichen History-Eintrag an, damit beim
|
||||||
// ersten Android-Back der popstate-Handler greifen kann (Toast „Nochmal
|
// ersten Android-Back der popstate-Handler greifen kann (Toast „Nochmal
|
||||||
|
|
@ -742,9 +739,8 @@ router.on('/orders/:id', async (args) => {
|
||||||
}
|
}
|
||||||
btn.textContent = '⏳';
|
btn.textContent = '⏳';
|
||||||
try {
|
try {
|
||||||
const t = await api.getToken();
|
const params = new URLSearchParams({ relpath: rel });
|
||||||
const params = new URLSearchParams({ relpath: rel, jwt: t });
|
const r = await fetch(window.location.origin + '/custom/bericht/api/photo.php?' + params.toString(), { credentials: 'same-origin' });
|
||||||
const r = await fetch(window.location.origin + '/custom/bericht/api/photo.php?' + params.toString());
|
|
||||||
if (!r.ok) throw new Error('Load failed');
|
if (!r.ok) throw new Error('Load failed');
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
const url = URL.createObjectURL(new Blob([blob], { type: mime }));
|
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
|
// Direkter Fetch statt api.confirmShipment, damit wir den Response-Body
|
||||||
// auch bei 500 lesen koennen
|
// 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 url = window.location.origin + '/custom/bericht/api/shipments.php?id=' + shipmentId + '&action=confirm';
|
||||||
const r = await fetch(url, {
|
const r = await fetch(url, {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: fd,
|
body: fd,
|
||||||
headers: { 'Authorization': 'Bearer ' + token },
|
credentials: 'same-origin',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' }, // State-Änderung -> CSRF-pflichtig
|
||||||
});
|
});
|
||||||
const text = await r.text();
|
const text = await r.text();
|
||||||
let payload;
|
let payload;
|
||||||
|
|
@ -2516,8 +2512,7 @@ async function openPdfViewer(blob, filename, relpath) {
|
||||||
modal.querySelector('#pdf-prev').onclick = () => renderPage(currentPage - 1);
|
modal.querySelector('#pdf-prev').onclick = () => renderPage(currentPage - 1);
|
||||||
modal.querySelector('#pdf-next').onclick = () => renderPage(currentPage + 1);
|
modal.querySelector('#pdf-next').onclick = () => renderPage(currentPage + 1);
|
||||||
modal.querySelector('#pdf-download').onclick = async () => {
|
modal.querySelector('#pdf-download').onclick = async () => {
|
||||||
const t = await api.getToken();
|
const params = new URLSearchParams({ relpath, download: 1 });
|
||||||
const params = new URLSearchParams({ relpath, jwt: t, download: 1 });
|
|
||||||
window.location.href = window.location.origin + '/custom/bericht/api/photo.php?' + params.toString();
|
window.location.href = window.location.origin + '/custom/bericht/api/photo.php?' + params.toString();
|
||||||
};
|
};
|
||||||
modal.querySelector('#pdf-share').onclick = () => {
|
modal.querySelector('#pdf-share').onclick = () => {
|
||||||
|
|
|
||||||
48
lib/api.js
48
lib/api.js
|
|
@ -24,9 +24,10 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function request(path, opts = {}) {
|
async function request(path, opts = {}) {
|
||||||
const t = await getToken();
|
|
||||||
const headers = opts.headers || {};
|
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)) {
|
if (!headers['Content-Type'] && !(opts.body instanceof FormData)) {
|
||||||
headers['Content-Type'] = 'application/json';
|
headers['Content-Type'] = 'application/json';
|
||||||
}
|
}
|
||||||
|
|
@ -41,7 +42,7 @@
|
||||||
}
|
}
|
||||||
let r;
|
let r;
|
||||||
try {
|
try {
|
||||||
r = await fetch(API_BASE + path, { ...opts, headers, signal });
|
r = await fetch(API_BASE + path, { ...opts, headers, signal, credentials: 'same-origin' });
|
||||||
} finally {
|
} finally {
|
||||||
if (toHandle) clearTimeout(toHandle);
|
if (toHandle) clearTimeout(toHandle);
|
||||||
}
|
}
|
||||||
|
|
@ -72,12 +73,14 @@
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
body: JSON.stringify({ login: loginName, password }),
|
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);
|
await idb.set('user', r.user);
|
||||||
return r;
|
return r;
|
||||||
}
|
}
|
||||||
|
|
||||||
async function logout() {
|
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();
|
await clearToken();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -242,10 +245,13 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getShipmentPdfBlobUrl(shipmentId) {
|
async function getShipmentPdfBlobUrl(shipmentId) {
|
||||||
const t = await getToken();
|
const params = new URLSearchParams({ id: shipmentId, action: 'pdf' });
|
||||||
if (!t) return null;
|
// shipments.php nutzt awlauth_require (CSRF) -> X-Requested-With nötig.
|
||||||
const params = new URLSearchParams({ id: shipmentId, action: 'pdf', jwt: t });
|
// Per fetch setzbar (kein window.location für das Shipment-PDF).
|
||||||
const r = await fetch(API_BASE + '/shipments.php?' + params.toString());
|
const r = await fetch(API_BASE + '/shipments.php?' + params.toString(), {
|
||||||
|
credentials: 'same-origin',
|
||||||
|
headers: { 'X-Requested-With': 'XMLHttpRequest' },
|
||||||
|
});
|
||||||
if (!r.ok) return null;
|
if (!r.ok) return null;
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
return { url: URL.createObjectURL(blob), blob };
|
return { url: URL.createObjectURL(blob), blob };
|
||||||
|
|
@ -271,31 +277,27 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
async function getPdfBlobUrl(berichtId) {
|
async function getPdfBlobUrl(berichtId) {
|
||||||
const t = await getToken();
|
// pdf.php nutzt awlauth_verify (kein CSRF) — Cookie reicht.
|
||||||
if (!t) return null;
|
const params = new URLSearchParams({ id: berichtId });
|
||||||
const params = new URLSearchParams({ id: berichtId, jwt: t });
|
const r = await fetch(API_BASE + '/pdf.php?' + params.toString(), { credentials: 'same-origin' });
|
||||||
const r = await fetch(API_BASE + '/pdf.php?' + params.toString());
|
|
||||||
if (!r.ok) return null;
|
if (!r.ok) return null;
|
||||||
const blob = await r.blob();
|
const blob = await r.blob();
|
||||||
return URL.createObjectURL(blob);
|
return URL.createObjectURL(blob);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Lädt eine Bild-Datei von der API als Blob-URL (inkl. JWT).
|
* Lädt eine Bild-Datei von der API als Blob-URL.
|
||||||
* Wird benötigt weil <img src> keine Authorization-Header mitschickt.
|
* Auth über das awl_sso-Cookie (same-origin) — photo.php nutzt awlauth_verify.
|
||||||
*/
|
*/
|
||||||
const blobUrlCache = new Map();
|
const blobUrlCache = new Map();
|
||||||
async function getPhotoBlobUrl(relpath, size) {
|
async function getPhotoBlobUrl(relpath, size) {
|
||||||
const key = (size || 'full') + '|' + relpath;
|
const key = (size || 'full') + '|' + relpath;
|
||||||
if (blobUrlCache.has(key)) return blobUrlCache.get(key);
|
if (blobUrlCache.has(key)) return blobUrlCache.get(key);
|
||||||
|
|
||||||
const t = await getToken();
|
const params = new URLSearchParams({ relpath });
|
||||||
if (!t) return null;
|
|
||||||
// JWT als Query-Param, weil Apache auf prod den Authorization-Header filtert
|
|
||||||
const params = new URLSearchParams({ relpath, jwt: t });
|
|
||||||
if (size) params.set('size', size);
|
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) {
|
if (!r.ok) {
|
||||||
const body = await r.text().catch(() => '');
|
const body = await r.text().catch(() => '');
|
||||||
console.warn('photo.php failed', r.status, relpath, body);
|
console.warn('photo.php failed', r.status, relpath, body);
|
||||||
|
|
@ -318,15 +320,13 @@
|
||||||
blobUrlCache.clear();
|
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.
|
// Ohne Mime-Filter — Aufrufer entscheidet selbst, was damit passiert.
|
||||||
async function getFileBlobUrl(relpath) {
|
async function getFileBlobUrl(relpath) {
|
||||||
const t = await getToken();
|
const params = new URLSearchParams({ relpath });
|
||||||
if (!t) return null;
|
|
||||||
const params = new URLSearchParams({ relpath, jwt: t });
|
|
||||||
const url = API_BASE + '/photo.php?' + params.toString();
|
const url = API_BASE + '/photo.php?' + params.toString();
|
||||||
console.log('[API] Fetching file:', relpath);
|
console.log('[API] Fetching file:', relpath);
|
||||||
const r = await fetch(url);
|
const r = await fetch(url, { credentials: 'same-origin' });
|
||||||
if (!r.ok) {
|
if (!r.ok) {
|
||||||
console.warn('[API] File fetch failed', r.status, relpath);
|
console.warn('[API] File fetch failed', r.status, relpath);
|
||||||
return null;
|
return null;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue