Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions api/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -424,6 +424,8 @@ client.connect().then(() => {
return;
}

res.setHeader("Cache-control", "no-store")

res.status(200).json({
csrfToken,
});
Expand Down
11 changes: 7 additions & 4 deletions api/src/middleware/csrfToken.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}
Expand Down
141 changes: 135 additions & 6 deletions frontend/src/Components/csrfHelper.jsx
Original file line number Diff line number Diff line change
@@ -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);
}
8 changes: 2 additions & 6 deletions frontend/src/Pages/Admin/adduser.jsx
Original file line number Diff line number Diff line change
@@ -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: "",
Expand Down Expand Up @@ -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",
Expand Down
14 changes: 4 additions & 10 deletions frontend/src/Pages/Admin/reports.jsx
Original file line number Diff line number Diff line change
@@ -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 = {
Expand Down Expand Up @@ -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: '',
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -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({
Expand Down
14 changes: 3 additions & 11 deletions frontend/src/Pages/Admin/rolereports.jsx
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -53,12 +53,6 @@ export default function RoleReports() {
"Gas Detection"
];

useEffect(() => {
if (csrfToken) {
sessionStorage.setItem("csrf", csrfToken);
}
}, [csrfToken]);

useEffect(() => {
fetchUsers();
}, []);
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 3 additions & 9 deletions frontend/src/Pages/Admin/users.jsx
Original file line number Diff line number Diff line change
@@ -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;

Expand All @@ -24,10 +24,6 @@ export default function Users() {

const csrfToken = useCsrfToken(apiUrl);

useEffect(() => {
if (csrfToken) sessionStorage.setItem("csrf", csrfToken);
}, [csrfToken]);

useEffect(() => {
fetchUsers();
}, []);
Expand Down Expand Up @@ -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),
})
Expand Down Expand Up @@ -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) })
})
Expand Down
11 changes: 2 additions & 9 deletions frontend/src/Pages/Attendance/Attendance_land.jsx
Original file line number Diff line number Diff line change
@@ -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;

Expand Down Expand Up @@ -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");
Expand Down Expand Up @@ -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,
Expand Down
Loading
Loading