From 9b87eb63aeeeb03a5df7b8ca697715bebee67214 Mon Sep 17 00:00:00 2001 From: jordancj Date: Sat, 1 Aug 2026 00:06:00 +1000 Subject: [PATCH 1/3] update conflicts from main --- api/src/index.ts | 142 ----------------------------------------------- 1 file changed, 142 deletions(-) diff --git a/api/src/index.ts b/api/src/index.ts index 3b94d88..4c08cda 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -330,148 +330,6 @@ client.connect().then(() => { ); }; - function normalizeNameKey(value: unknown): string { - return String(value ?? "") - .trim() - .replace(/\s+/g, " ") - .toLowerCase(); - } - - function escapeRegex(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - } - - function caseAndSpaceInsensitiveExactFilter(value: unknown) { - const parts = String(value ?? "") - .trim() - .split(/\s+/) - .filter(Boolean) - .map(escapeRegex); - - return { - $regex: - parts.length > 0 - ? `^\\s*${parts.join("\\s+")}\\s*$` - : "^\\s*$", - $options: "i", - }; - } - - function normalizeRoleReportKey(value: unknown): string { - return String(value ?? "") - .trim() - .replace(/\s+/g, " ") - .toLowerCase(); - } - - function cleanRoleReportDisplayValue(value: unknown): string { - return String(value ?? "") - .trim() - .replace(/\s+/g, " "); - } - - function escapeRoleReportRegex(value: string): string { - return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); - } - - function roleReportCaseAndSpaceInsensitiveFilter( - value: unknown - ) { - const parts = String(value ?? "") - .trim() - .split(/\s+/) - .filter(Boolean) - .map(escapeRoleReportRegex); - - return { - $regex: - parts.length > 0 - ? `^\\s*${parts.join("\\s+")}\\s*$` - : "^\\s*$", - $options: "i", - }; - } - - function buildRoleReportQuery( - startEpoch: number, - endEpoch: number, - names: unknown, - roles: unknown - ) { - const query: any = { - epochTimestamp: { - $gte: startEpoch, - $lte: endEpoch, - }, - roles: { - $exists: true, - $ne: [], - }, - }; - - const selectionClauses: any[] = []; - - if (Array.isArray(names) && names.length > 0) { - selectionClauses.push({ - $or: names.map((selectedName: unknown) => ({ - name: roleReportCaseAndSpaceInsensitiveFilter( - selectedName - ), - })), - }); - } - - if (Array.isArray(roles) && roles.length > 0) { - selectionClauses.push({ - $or: roles.map((selectedRole: unknown) => ({ - roles: roleReportCaseAndSpaceInsensitiveFilter( - selectedRole - ), - })), - }); - } - - if (selectionClauses.length > 0) { - query.$and = selectionClauses; - } - - return query; - } - - function buildRoleReportDisplayMap( - values: unknown[] - ): Map { - const displayByKey = new Map(); - - for (const value of values) { - const key = normalizeRoleReportKey(value); - - if (!key || displayByKey.has(key)) { - continue; - } - - displayByKey.set( - key, - cleanRoleReportDisplayValue(value) - ); - } - - return displayByKey; - } - - function roleReportRecordHasRoleKey( - record: any, - roleKey: string - ): boolean { - return ( - Array.isArray(record.roles) && - record.roles.some( - (recordRole: unknown) => - normalizeRoleReportKey(recordRole) === roleKey - ) - ); - } - async function sendXlsxResponse( res: Response, filename: string, From a67ea760505fca7dfd985ebfa4481441ccde33bc Mon Sep 17 00:00:00 2001 From: jordancj Date: Sun, 9 Aug 2026 12:48:31 +1000 Subject: [PATCH 2/3] Fix csrf flows and stale tokens --- api/src/index.ts | 2 + api/src/middleware/csrfToken.ts | 11 +- frontend/src/Components/csrfHelper.jsx | 141 +++++++++++++++++- frontend/src/Pages/Admin/adduser.jsx | 8 +- frontend/src/Pages/Admin/reports.jsx | 14 +- frontend/src/Pages/Admin/rolereports.jsx | 14 +- frontend/src/Pages/Admin/users.jsx | 12 +- .../src/Pages/Attendance/Attendance_land.jsx | 11 +- .../src/Pages/Attendance/Manage-Incidents.jsx | 22 +-- frontend/src/Pages/Attendance/Non-op-page.jsx | 8 +- frontend/src/Pages/Attendance/Op-page.jsx | 9 +- frontend/src/Pages/Attendance/Roles.jsx | 21 +-- 12 files changed, 174 insertions(+), 99 deletions(-) diff --git a/api/src/index.ts b/api/src/index.ts index 4c08cda..39bb5dd 100644 --- a/api/src/index.ts +++ b/api/src/index.ts @@ -424,6 +424,8 @@ client.connect().then(() => { return; } + res.setHeader("Cache-control", "no-store") + res.status(200).json({ csrfToken, }); diff --git a/api/src/middleware/csrfToken.ts b/api/src/middleware/csrfToken.ts index 4a5e621..9aafbed 100644 --- a/api/src/middleware/csrfToken.ts +++ b/api/src/middleware/csrfToken.ts @@ -21,23 +21,26 @@ export const csrfMiddleware: RequestHandler = (req, res, next) => { const sent = req.get(CSRF_HEADER); - if (typeof sent !== "string") { + if (typeof sent !== "string" || !sent) { res.status(403).json({ - message: "Missing CSRF token. Refresh and try again", + code: "CSRF_HEADER_MISSING", + message: "Missing CSRF token.", }); return; } if (typeof req.session.csrfToken !== "string") { res.status(403).json({ - message: "CSRF session token missing. Refresh and try again", + code: "CSRF_SESSION_TOKEN_MISSING", + message: "CSRF session token missing.", }); return; } if (sent !== req.session.csrfToken) { res.status(403).json({ - message: "Invalid CSRF token. Refresh and try again.", + code: "CSRF_TOKEN_INVALID", + message: "Invalid CSRF token.", }); return; } diff --git a/frontend/src/Components/csrfHelper.jsx b/frontend/src/Components/csrfHelper.jsx index 7b5c710..05ddcc2 100644 --- a/frontend/src/Components/csrfHelper.jsx +++ b/frontend/src/Components/csrfHelper.jsx @@ -1,14 +1,143 @@ -import { useEffect, useState } from 'react'; +import { useEffect, useState } from "react"; + +let cachedCsrfToken = ""; +let csrfRequestPromise = null; + +async function fetchCsrfToken(apiurl) { + const response = await fetch( + `${apiurl}/csrf-token`, + { + method: "GET", + credentials: "include", + cache: "no-store", + headers: { + Accept: "application/json", + }, + } + ); + + if (!response.ok) { + throw new Error( + `CSRF request failed: ${response.status}` + ); + } + + const data = await response.json(); + + if ( + typeof data.csrfToken !== "string" || + !data.csrfToken + ) { + throw new Error( + "CSRF token missing from response" + ); + } + + return data.csrfToken; +} export function useCsrfToken(apiurl) { - const [token, setToken] = useState(''); + const [token, setToken] = useState(""); useEffect(() => { - fetch(`${apiurl}/csrf-token`, { credentials: 'include' }) - .then(res => res.json()) - .then(data => setToken(data.csrfToken)) - .catch(console.error); + let active = true; + + fetchCsrfToken(apiurl) + .then((csrfToken) => { + if (active) { + setToken(csrfToken); + } + }) + .catch((error) => { + console.error( + "Failed to fetch CSRF token:", + error + ); + + if (active) { + setToken(""); + } + }); + + return () => { + active = false; + }; }, [apiurl]); return token; } + +export async function csrfFetch( + apiurl, + path, + options = {} +) { + const method = String( + options.method || "GET" + ).toUpperCase(); + + const unsafe = [ + "POST", + "PUT", + "PATCH", + "DELETE", + ].includes(method); + + const makeRequest = async (token = "") => { + const headers = new Headers( + options.headers || {} + ); + + if (unsafe) { + headers.set( + "X-CSRF-Token", + token + ); + } + + return fetch(`${apiurl}${path}`, { + ...options, + method, + credentials: "include", + headers, + }); + }; + + if (!unsafe) { + return makeRequest(); + } + + // Always ask the server for the token + // belonging to the current session. + let token = await fetchCsrfToken(apiurl); + + let response = await makeRequest(token); + + if (response.status !== 403) { + return response; + } + + let errorData; + + try { + errorData = await response.clone().json(); + } catch { + return response; + } + + const csrfError = + errorData?.code === "CSRF_HEADER_MISSING" || + errorData?.code === "CSRF_SESSION_TOKEN_MISSING" || + errorData?.code === "CSRF_TOKEN_INVALID"; + + if (!csrfError) { + return response; + } + + // Ask the server again for the CSRF token + // associated with whatever session now exists. + token = await fetchCsrfToken(apiurl); + + // Retry once only. + return makeRequest(token); +} \ No newline at end of file diff --git a/frontend/src/Pages/Admin/adduser.jsx b/frontend/src/Pages/Admin/adduser.jsx index 7bb2075..7544dd2 100644 --- a/frontend/src/Pages/Admin/adduser.jsx +++ b/frontend/src/Pages/Admin/adduser.jsx @@ -1,14 +1,11 @@ import { useTitle } from '../../hooks/useTitle.jsx'; import {useState, useEffect} from "react"; -import {useCsrfToken} from "../../Components/csrfHelper.jsx" +import {useCsrfToken, csrfFetch} from "../../Components/csrfHelper.jsx" const apiUrl = import.meta.env.VITE_API_BASE_URL const AddUser = () => { const csrfToken = useCsrfToken(apiUrl); - useEffect(() => { - if (csrfToken) sessionStorage.setItem("csrf", csrfToken); - }, [csrfToken]); const [formData, setFormData] = useState({ firstName: "", @@ -47,10 +44,9 @@ const AddUser = () => { return; } try { - const res = await fetch(`${import.meta.env.VITE_API_BASE_URL}/api/users/adduser`, { + const res = await csrfFetch(apiUrl, "/api/users/adduser", { method: "POST", headers: { - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), "Content-Type": "application/json", }, credentials: "include", diff --git a/frontend/src/Pages/Admin/reports.jsx b/frontend/src/Pages/Admin/reports.jsx index f509f6c..88af697 100644 --- a/frontend/src/Pages/Admin/reports.jsx +++ b/frontend/src/Pages/Admin/reports.jsx @@ -1,7 +1,7 @@ import { useTitle } from '../../hooks/useTitle.jsx'; import { useEffect, useState } from 'react'; import moment from 'moment-timezone'; -import {useCsrfToken} from "../../Components/csrfHelper.jsx" +import {useCsrfToken, csrfFetch} from "../../Components/csrfHelper.jsx" const apiUrl = import.meta.env.VITE_API_BASE_URL; const activityOptions = { @@ -41,10 +41,6 @@ const activityOptions = { export default function Reports({ users = [] }) { - const csrfToken = useCsrfToken(apiUrl); - useEffect(() => { - if (csrfToken) sessionStorage.setItem("csrf", csrfToken); - }, [csrfToken]); const [errorMessage, setErrorMessage] = useState(""); const [form, setForm] = useState({ startTime: '', @@ -87,11 +83,10 @@ export default function Reports({ users = [] }) { const startEpoch = moment.tz(form.startTime, "Australia/Sydney").valueOf(); const endEpoch = moment.tz(form.endTime, "Australia/Sydney").valueOf(); - const res = await fetch(`${apiUrl}/api/reports/run`, { + const res = await csrfFetch(apiUrl, "/api/reports/run", { method: 'POST', headers: { - 'Content-Type': 'application/json', - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), + 'Content-Type': 'application/json', }, credentials: "include", body: JSON.stringify({ @@ -193,11 +188,10 @@ export default function Reports({ users = [] }) { const formattedStart = start.format('YYYYMMDD'); const formattedEnd = end.format('YYYYMMDD'); - const res = await fetch(`${apiUrl}/api/reports/export`, { + const res = await csrfFetch(apiUrl, "/api/reports/export", { method: 'POST', headers: { 'Content-Type': 'application/json', - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf") }, credentials: "include", body: JSON.stringify({ diff --git a/frontend/src/Pages/Admin/rolereports.jsx b/frontend/src/Pages/Admin/rolereports.jsx index 4d82b32..037ca74 100644 --- a/frontend/src/Pages/Admin/rolereports.jsx +++ b/frontend/src/Pages/Admin/rolereports.jsx @@ -1,7 +1,7 @@ import { useTitle } from "../../hooks/useTitle.jsx"; import { useEffect, useState } from "react"; import moment from "moment-timezone"; -import { useCsrfToken } from "../../Components/csrfHelper.jsx"; +import { useCsrfToken, csrfFetch } from "../../Components/csrfHelper.jsx"; const apiUrl = import.meta.env.VITE_API_BASE_URL; @@ -53,12 +53,6 @@ export default function RoleReports() { "Gas Detection" ]; - useEffect(() => { - if (csrfToken) { - sessionStorage.setItem("csrf", csrfToken); - } - }, [csrfToken]); - useEffect(() => { fetchUsers(); }, []); @@ -191,12 +185,11 @@ export default function RoleReports() { setIsRunning(true); try { - const response = await fetch(`${apiUrl}/api/reports/roles/run`, { + const response = await csrfFetch(apiUrl, "/api/reports/roles/run", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ startEpoch, @@ -243,12 +236,11 @@ export default function RoleReports() { setIsExporting(true); try { - const response = await fetch(`${apiUrl}/api/reports/roles/export`, { + const response = await csrfFetch(apiUrl, "/api/reports/roles/export", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ startEpoch, diff --git a/frontend/src/Pages/Admin/users.jsx b/frontend/src/Pages/Admin/users.jsx index 110f05c..13d4a7d 100644 --- a/frontend/src/Pages/Admin/users.jsx +++ b/frontend/src/Pages/Admin/users.jsx @@ -1,6 +1,6 @@ import { useTitle } from '../../hooks/useTitle.jsx'; import { useState, useEffect } from 'react'; -import {useCsrfToken} from "../../Components/csrfHelper.jsx" +import {useCsrfToken, csrfFetch} from "../../Components/csrfHelper.jsx" const apiUrl = import.meta.env.VITE_API_BASE_URL; @@ -24,10 +24,6 @@ export default function Users() { const csrfToken = useCsrfToken(apiUrl); - useEffect(() => { - if (csrfToken) sessionStorage.setItem("csrf", csrfToken); - }, [csrfToken]); - useEffect(() => { fetchUsers(); }, []); @@ -78,12 +74,11 @@ export default function Users() { memberClassification: editingUser.membership_classification, memberType: editingUser.membership_type, }; - fetch(`${apiUrl}/api/users/updateRecord`, { + csrfFetch(apiUrl, "/api/users/updateRecord", { method: 'PATCH', credentials: "include", headers: { 'Content-Type': 'application/json', - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf") }, body: JSON.stringify(updatedData), }) @@ -114,12 +109,11 @@ export default function Users() { const handleDelete = async () => { if (!selectedUsers.size || !window.confirm("Are you sure you want to delete selected users?")) return; - fetch(`${apiUrl}/api/users/delete`, { + csrfFetch(apiUrl, "/api/users/delete", { method: 'DELETE', credentials: "include", headers: { 'Content-Type': 'application/json', - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ numbers: Array.from(selectedUsers) }) }) diff --git a/frontend/src/Pages/Attendance/Attendance_land.jsx b/frontend/src/Pages/Attendance/Attendance_land.jsx index 39b5d81..df3b001 100644 --- a/frontend/src/Pages/Attendance/Attendance_land.jsx +++ b/frontend/src/Pages/Attendance/Attendance_land.jsx @@ -1,6 +1,6 @@ import { useTitle } from "../../hooks/useTitle.jsx"; import { useState, useEffect, useRef } from "react"; -import { useCsrfToken } from "../../Components/csrfHelper.jsx"; +import { useCsrfToken, csrfFetch } from "../../Components/csrfHelper.jsx"; const apiurl = import.meta.env.VITE_API_BASE_URL; @@ -54,12 +54,6 @@ export default function Login() { paddingBottom: "2rem", }; - useEffect(() => { - if (csrfToken) { - sessionStorage.setItem("csrf", csrfToken); - } - }, [csrfToken]); - useEffect(() => { const params = new URLSearchParams(window.location.search); const message = params.get("popupMessage"); @@ -152,12 +146,11 @@ export default function Login() { } try { - const res = await fetch(`${apiurl}/api/attendance/checkUser`, { + const res = await csrfFetch(apiurl, "/api/attendance/checkUser", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ usernames: selectedUsernames, diff --git a/frontend/src/Pages/Attendance/Manage-Incidents.jsx b/frontend/src/Pages/Attendance/Manage-Incidents.jsx index 4aae646..0c9fb85 100644 --- a/frontend/src/Pages/Attendance/Manage-Incidents.jsx +++ b/frontend/src/Pages/Attendance/Manage-Incidents.jsx @@ -1,6 +1,6 @@ import { useState, useEffect } from "react"; import { useTitle } from "../../hooks/useTitle.jsx"; -import { useCsrfToken } from "../../Components/csrfHelper.jsx"; +import { useCsrfToken, csrfFetch } from "../../Components/csrfHelper.jsx"; import { validateIncidentCreationForm } from "../../Utils/formValidation.js"; const apiurl = import.meta.env.VITE_API_BASE_URL; @@ -36,11 +36,6 @@ export default function CreateIncidentsPage() { overflowX: "hidden", paddingBottom: "2rem", }; - useEffect(() => { - if (csrfToken) { - sessionStorage.setItem("csrf", csrfToken); - } - }, [csrfToken]); useEffect(() => { const checkPinStatus = async () => { @@ -101,7 +96,6 @@ export default function CreateIncidentsPage() { credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, }); @@ -129,7 +123,6 @@ export default function CreateIncidentsPage() { credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, }); @@ -184,12 +177,11 @@ export default function CreateIncidentsPage() { }; try { - const response = await fetch(`${apiurl}/api/attendance/createIncident`, { + const response = await csrfFetch(apiurl, "/api/attendance/createIncident", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify(payload), }); @@ -231,14 +223,13 @@ export default function CreateIncidentsPage() { setSubmitStatus(null); try { - const response = await fetch( - `${apiurl}/api/attendance/deleteIncident/${eventNumberToDelete}`, + const response = await csrfFetch( + apiurl, `/api/attendance/deleteIncident/${eventNumberToDelete}`, { method: "DELETE", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, } ); @@ -288,14 +279,13 @@ export default function CreateIncidentsPage() { setIsDeleting(true); try { - const response = await fetch( - `${apiurl}/api/attendance/roleAssignment/unlock`, + const response = await csrfFetch( + apiurl, "/api/attendance/roleAssignment/unlock", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ pin }), } diff --git a/frontend/src/Pages/Attendance/Non-op-page.jsx b/frontend/src/Pages/Attendance/Non-op-page.jsx index 6a2aa0f..47e19cc 100644 --- a/frontend/src/Pages/Attendance/Non-op-page.jsx +++ b/frontend/src/Pages/Attendance/Non-op-page.jsx @@ -1,7 +1,7 @@ import { useNavigate } from "react-router-dom"; import { useTitle } from '../../hooks/useTitle.jsx'; import {useState, useEffect } from "react"; -import {useCsrfToken} from "../../Components/csrfHelper.jsx" +import {useCsrfToken, csrfFetch} from "../../Components/csrfHelper.jsx" const activities = [ "Meeting", @@ -22,9 +22,6 @@ const pageShellStyle = { export default function OperationalPage() { const csrfToken = useCsrfToken(apiurl); - useEffect(() => { - if (csrfToken) sessionStorage.setItem("csrf", csrfToken); - }, [csrfToken]); const [otherType, setOtherType] = useState("") const [selectedActivity, setSelectedActivity] = useState(sessionStorage.getItem("activity") || ""); const [date, setDate] = useState(""); @@ -112,12 +109,11 @@ export default function OperationalPage() { epochTimestamp: dateObj.getTime(), ...(activity === "Other-Non-operational" && { otherType }), }; - const response = await fetch(`${apiurl}/api/attendance/submit`, { + const response = await csrfFetch(apiurl, "/api/attendance/submit", { method: "POST", credentials: 'include', headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify(data), }); diff --git a/frontend/src/Pages/Attendance/Op-page.jsx b/frontend/src/Pages/Attendance/Op-page.jsx index fb90a50..980d05a 100644 --- a/frontend/src/Pages/Attendance/Op-page.jsx +++ b/frontend/src/Pages/Attendance/Op-page.jsx @@ -1,7 +1,7 @@ import {useState, useEffect} from "react"; import { useNavigate } from "react-router-dom"; import { useTitle } from '../../hooks/useTitle.jsx'; -import {useCsrfToken} from "../../Components/csrfHelper.jsx" +import {useCsrfToken, csrfFetch} from "../../Components/csrfHelper.jsx" import { validateOperationalAttendanceData } from "../../Utils/formValidation.js"; const activities = [ @@ -30,9 +30,6 @@ const pageShellStyle = { export default function OperationalPage() { const csrfToken = useCsrfToken(apiurl); - useEffect(() => { - if (csrfToken) sessionStorage.setItem("csrf", csrfToken); - }, [csrfToken]); const [selectedActivity, setSelectedActivity] = useState(sessionStorage.getItem("activity") || ""); const [baType, setBaType] = useState(""); const [chainsawType, setChainsawType] = useState(""); @@ -64,7 +61,6 @@ export default function OperationalPage() { credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, }); @@ -140,12 +136,11 @@ export default function OperationalPage() { setIsSubmitting(true) setSubmitMessage(null) setSubmitStatus(null) - const response = await fetch(`${apiurl}/api/attendance/submit`, { + const response = await csrfFetch(apiurl, "/api/attendance/submit", { method: "POST", credentials: 'include', headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf") }, body: JSON.stringify(data), }); diff --git a/frontend/src/Pages/Attendance/Roles.jsx b/frontend/src/Pages/Attendance/Roles.jsx index 76edfca..15bfd8f 100644 --- a/frontend/src/Pages/Attendance/Roles.jsx +++ b/frontend/src/Pages/Attendance/Roles.jsx @@ -1,6 +1,6 @@ import { useEffect, useState } from "react"; import { useTitle } from "../../hooks/useTitle.jsx"; -import { useCsrfToken } from "../../Components/csrfHelper.jsx"; +import { useCsrfToken, csrfFetch } from "../../Components/csrfHelper.jsx"; const apiurl = import.meta.env.VITE_API_BASE_URL; @@ -174,12 +174,6 @@ export default function RoleAssignment() { setMessage({ type, text }); }; - useEffect(() => { - if (csrfToken) { - sessionStorage.setItem("csrf", csrfToken); - } - }, [csrfToken]); - useEffect(() => { if (!message) return; @@ -443,14 +437,13 @@ export default function RoleAssignment() { } try { - const response = await fetch( - `${apiurl}/api/attendance/roleAssignment/unlock`, + const response = await csrfFetch( + apiurl, "/api/attendance/roleAssignment/unlock", { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ pin }), } @@ -493,14 +486,13 @@ export default function RoleAssignment() { setMessage(null); try { - const response = await fetch( - `${apiurl}/api/attendance/roleAssignment/updateRoles`, + const response = await csrfFetch( + apiurl, "/api/attendance/roleAssignment/updateRoles", { method: "PATCH", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ eventNumber: selectedEventNumber, @@ -541,14 +533,13 @@ export default function RoleAssignment() { setMessage(null); try { - const response = await fetch( + const response = await csrfFetch( `${apiurl}/api/attendance/roleAssignment/addAttendance`, { method: "POST", credentials: "include", headers: { "Content-Type": "application/json", - "X-CSRF-Token": csrfToken || sessionStorage.getItem("csrf"), }, body: JSON.stringify({ eventNumber: selectedEventNumber, From d06789ba7907bba1acc69de8edef156f7c194779 Mon Sep 17 00:00:00 2001 From: jordancj Date: Sun, 9 Aug 2026 13:12:50 +1000 Subject: [PATCH 3/3] Update leftover endpoint --- frontend/src/Pages/Attendance/Roles.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/frontend/src/Pages/Attendance/Roles.jsx b/frontend/src/Pages/Attendance/Roles.jsx index 15bfd8f..9dc955e 100644 --- a/frontend/src/Pages/Attendance/Roles.jsx +++ b/frontend/src/Pages/Attendance/Roles.jsx @@ -534,7 +534,7 @@ export default function RoleAssignment() { try { const response = await csrfFetch( - `${apiurl}/api/attendance/roleAssignment/addAttendance`, + apiurl, "/api/attendance/roleAssignment/addAttendance", { method: "POST", credentials: "include",