diff --git a/app/Views/reimbursements/under_processing.php b/app/Views/reimbursements/under_processing.php index b202753..54fe14f 100644 --- a/app/Views/reimbursements/under_processing.php +++ b/app/Views/reimbursements/under_processing.php @@ -373,8 +373,94 @@ let tempBatchCounter = -1; let csrfValue = cfg.csrfHash || ''; + const csrfCookieName = csrfCookieName ?? (config('Security')->cookieName ?? 'csrf_cookie_name')) ?>; + let csrfRequestQueue = Promise.resolve(); let dragPayload = null; + function getCookie(name) { + const parts = document.cookie.split(';'); + for (let i = 0; i < parts.length; i += 1) { + const part = parts[i].trim(); + if (part.startsWith(`${name}=`)) { + return decodeURIComponent(part.slice(name.length + 1)); + } + } + return ''; + } + + function syncCsrfFromCookie() { + if (!csrfCookieName) { + return csrfValue; + } + const cookieValue = getCookie(csrfCookieName); + if (cookieValue) { + csrfValue = cookieValue; + } + return csrfValue; + } + + function updateCsrfToken(response, data = null) { + const headerValue = response?.headers?.get?.('X-CSRF-HASH') || ''; + if (headerValue) { + csrfValue = headerValue; + return; + } + const bodyValue = data && typeof data === 'object' ? (data.csrf_hash || data.csrfHash || '') : ''; + if (bodyValue) { + csrfValue = bodyValue; + return; + } + syncCsrfFromCookie(); + } + + function withSerializedCsrfRequest(task) { + const run = csrfRequestQueue + .catch(() => undefined) + .then(() => task()); + csrfRequestQueue = run.then(() => undefined, () => undefined); + return run; + } + + async function postJson(url, form, fallbackError) { + return withSerializedCsrfRequest(async () => { + syncCsrfFromCookie(); + if (cfg.csrfToken) { + form.set(cfg.csrfToken, csrfValue); + } + + const headers = { 'X-Requested-With': 'XMLHttpRequest' }; + if (csrfValue) { + headers['X-CSRF-TOKEN'] = csrfValue; + } + + const response = await fetch(url, { + method: 'POST', + headers, + credentials: 'same-origin', + body: form, + }); + + const text = await response.text(); + let data = null; + try { + data = text ? JSON.parse(text) : {}; + } catch (_) { + data = { + success: false, + error: text || fallbackError || 'Unexpected response', + }; + } + + updateCsrfToken(response, data); + + if (!response.ok || !data.success) { + throw new Error(data.error || fallbackError || 'Request failed.'); + } + + return data; + }); + } + function openReceiptModal(url) { if (!url) { return; @@ -470,23 +556,8 @@ } const form = new FormData(); form.append('expense_id', itemId); - if (cfg.csrfToken) { - form.append(cfg.csrfToken, csrfValue); - } try { - const response = await fetch(cfg.markDonationUrl, { - method: 'POST', - headers: { 'X-Requested-With': 'XMLHttpRequest' }, - body: form, - }); - const data = await response.json().catch(() => ({ success: false, error: 'Unexpected response' })); - const newHash = response.headers.get('X-CSRF-HASH') || data.csrf_hash; - if (newHash) { - csrfValue = newHash; - } - if (!response.ok || !data.success) { - throw new Error(data.error || 'Unable to mark donation right now.'); - } + await postJson(cfg.markDonationUrl, form, 'Unable to mark donation right now.'); removeItemFromUI(itemId); } catch (err) { console.error(err); @@ -737,23 +808,7 @@ } else { form.append('admin_id', ''); } - if (cfg.csrfToken) { - form.append(cfg.csrfToken, csrfValue); - } - - fetch(cfg.updateUrl, { - method: 'POST', - headers: { 'X-Requested-With': 'XMLHttpRequest' }, - body: form, - }).then(async (response) => { - const data = await response.json().catch(() => ({ success: false, error: 'Unexpected response' })); - const newHash = response.headers.get('X-CSRF-HASH') || data.csrf_hash; - if (newHash) { - csrfValue = newHash; - } - if (!response.ok || !data.success) { - throw new Error(data.error || 'Unable to update batch assignment.'); - } + postJson(cfg.updateUrl, form, 'Unable to update batch assignment.').then((data) => { entry.batch_number = batchId; entry.batchId = batchId; entry.admin_id = (adminId || adminId === 0) ? adminId : null; @@ -791,25 +846,7 @@ throw new Error('Batch creation endpoint is not configured.'); } const form = new FormData(); - if (cfg.csrfToken) { - form.append(cfg.csrfToken, csrfValue); - } - - const response = await fetch(cfg.createBatchUrl, { - method: 'POST', - headers: { 'X-Requested-With': 'XMLHttpRequest' }, - body: form, - }); - const data = await response.json().catch(() => ({ success: false, error: 'Unexpected response when creating batch.' })); - const newHash = response.headers.get('X-CSRF-HASH') || data.csrf_hash; - if (newHash) { - csrfValue = newHash; - } - if (!response.ok || !data.success) { - throw new Error(data.error || 'Unable to create a new batch right now.'); - } - - return data; + return postJson(cfg.createBatchUrl, form, 'Unable to create a new batch right now.'); } async function uploadAdminCheckFile(batchId, adminId, input, statusEl, linkEl) { @@ -827,28 +864,13 @@ form.append('batch_id', batchId); form.append('admin_id', adminId ?? 0); form.append('check_file', file); - if (cfg.csrfToken) { - form.append(cfg.csrfToken, csrfValue); - } input.disabled = true; const previous = statusEl.textContent; statusEl.textContent = 'Uploading...'; try { - const response = await fetch(cfg.checkUploadUrl, { - method: 'POST', - headers: { 'X-Requested-With': 'XMLHttpRequest' }, - body: form, - }); - const data = await response.json().catch(() => ({ success: false, error: 'Unexpected response' })); - const newHash = response.headers.get('X-CSRF-HASH') || data.csrf_hash; - if (newHash) { - csrfValue = newHash; - } - if (!response.ok || !data.success) { - throw new Error(data.error || 'Unable to upload check file right now.'); - } + const data = await postJson(cfg.checkUploadUrl, form, 'Unable to upload check file right now.'); const label = data.original_filename || 'View check'; statusEl.textContent = `Uploaded: ${data.original_filename || 'file'}`; if (linkEl && data.url) { @@ -979,34 +1001,19 @@ } const form = new FormData(); form.append('batch_id', batchId); - if (cfg.csrfToken) { - form.append(cfg.csrfToken, csrfValue); - } if (button) { button.disabled = true; button.textContent = 'Locking...'; } try { - const response = await fetch(cfg.lockBatchUrl, { - method: 'POST', - headers: { 'X-Requested-With': 'XMLHttpRequest' }, - body: form, - }); - const data = await response.json().catch(() => ({ success: false, error: 'Unexpected response' })); - const newHash = response.headers.get('X-CSRF-HASH') || data.csrf_hash; - if (newHash) { - csrfValue = newHash; - } - if (!response.ok || !data.success) { - throw new Error(data.error || 'Unable to lock batch at the moment.'); - } + await postJson(cfg.lockBatchUrl, form, 'Unable to lock batch at the moment.'); markBatchCardLocked(card); } catch (err) { console.error(err); alert(err.message || 'Unable to lock the batch. Please try again.'); if (button) { button.disabled = false; - button.textContent = 'Lock Batch'; + button.textContent = 'Submit Batch'; } } }