From 11aab231ef6e9663c230dd320a1d099ad2d550d4 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Sat, 20 Apr 2024 15:29:31 +0200 Subject: [PATCH 1/9] fixed invalid_token_loader and added function to get csrf_cookie --- backend/project/init_auth.py | 2 +- frontend/src/utils/crsf.ts | 10 ++++++++++ 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 frontend/src/utils/crsf.ts diff --git a/backend/project/init_auth.py b/backend/project/init_auth.py index bc20c497..5d4d6356 100644 --- a/backend/project/init_auth.py +++ b/backend/project/init_auth.py @@ -19,7 +19,7 @@ def expired_token_callback(jwt_header, jwt_payload): 401) @jwt.invalid_token_loader - def invalid_token_callback(jwt_header, jwt_payload): + def invalid_token_callback(jwt_header): return ( {"message":("The server cannot recognize this access token cookie, " "please log in again if you think this is an error")}, diff --git a/frontend/src/utils/crsf.ts b/frontend/src/utils/crsf.ts new file mode 100644 index 00000000..50f73ea9 --- /dev/null +++ b/frontend/src/utils/crsf.ts @@ -0,0 +1,10 @@ +export function get_csrf_cookie(): string { + const cookie = document.cookie + .split("; ") + .find((row) => row.startsWith("csrf_access_token=")) + ?.split("=")[1]; + if(!cookie) { + return ""; + } + return cookie; +} \ No newline at end of file From c9b534d995fb01015bb78860e94436763b7d0f6c Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Sat, 20 Apr 2024 15:31:19 +0200 Subject: [PATCH 2/9] changed name of file to be correct --- frontend/src/utils/{crsf.ts => csrf.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename frontend/src/utils/{crsf.ts => csrf.ts} (100%) diff --git a/frontend/src/utils/crsf.ts b/frontend/src/utils/csrf.ts similarity index 100% rename from frontend/src/utils/crsf.ts rename to frontend/src/utils/csrf.ts From 14084135323959e9ed26cc0dc4da6abffb792705 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Mon, 22 Apr 2024 15:21:57 +0200 Subject: [PATCH 3/9] small fixes --- backend/project/endpoints/authentication/auth.py | 6 ++++++ frontend/src/components/Header/Login.tsx | 2 +- frontend/src/utils/csrf.ts | 11 ++++++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/backend/project/endpoints/authentication/auth.py b/backend/project/endpoints/authentication/auth.py index 86bf96dc..9d078139 100644 --- a/backend/project/endpoints/authentication/auth.py +++ b/backend/project/endpoints/authentication/auth.py @@ -45,6 +45,12 @@ def microsoft_authentication(): res = requests.post(f"https://login.microsoftonline.com/{TENANT_ID}/oauth2/v2.0/token", data=data, timeout=5) + if res.status_code != 200: + abort(make_response(( + {"message": + "An error occured while trying to authenticate your access token"}, + 500))) + # hier wel nog if om error zelf op te vangen token = res.json()["access_token"] profile_res = requests.get("https://graph.microsoft.com/v1.0/me", headers={"Authorization":f"Bearer {token}"}, diff --git a/frontend/src/components/Header/Login.tsx b/frontend/src/components/Header/Login.tsx index d06bf021..496273c3 100644 --- a/frontend/src/components/Header/Login.tsx +++ b/frontend/src/components/Header/Login.tsx @@ -2,7 +2,7 @@ import {Button} from "@mui/material"; import { Link } from 'react-router-dom'; const CLIENT_ID = import.meta.env.VITE_APP_CLIENT_ID; -const REDIRECT_URI = encodeURI(import.meta.env.VITE_APP_API_HOST + "/auth"); +const REDIRECT_URI = encodeURIComponent(import.meta.env.VITE_APP_API_HOST + "/auth"); const TENANT_ID = import.meta.env.VITE_APP_TENANT_ID; /** diff --git a/frontend/src/utils/csrf.ts b/frontend/src/utils/csrf.ts index 50f73ea9..cce72dca 100644 --- a/frontend/src/utils/csrf.ts +++ b/frontend/src/utils/csrf.ts @@ -1,10 +1,11 @@ +/** + * A helper function to easily retrieve the crsf_access_token cookie + * @returns the crsf_access_token cookie + */ export function get_csrf_cookie(): string { - const cookie = document.cookie + const cookie = document.cookie .split("; ") .find((row) => row.startsWith("csrf_access_token=")) ?.split("=")[1]; - if(!cookie) { - return ""; - } - return cookie; + return cookie ? cookie : ""; } \ No newline at end of file From 1c669196c16d27e7bbbf51b590f5a153cdf23c95 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Thu, 25 Apr 2024 15:01:09 +0200 Subject: [PATCH 4/9] added csrf to all fetches --- .../Courses/CourseDetailTeacher.tsx | 19 +++++++++++--- .../Courses/CourseUtilComponents.tsx | 8 +++++- .../src/components/Courses/CourseUtils.tsx | 7 +++-- .../components/ProjectForm/ProjectForm.tsx | 11 ++++++-- .../ProjectSubmissionOverview.tsx | 9 ++++++- .../ProjectSubmissionOverviewDatagrid.tsx | 6 +++++ frontend/src/pages/project/FetchProjects.tsx | 26 +++++++++++++++---- .../pages/project/projectView/ProjectView.tsx | 10 +++++++ .../project/projectView/SubmissionCard.tsx | 6 ++++- 9 files changed, 86 insertions(+), 16 deletions(-) diff --git a/frontend/src/components/Courses/CourseDetailTeacher.tsx b/frontend/src/components/Courses/CourseDetailTeacher.tsx index e1cf0ff3..28d2819b 100644 --- a/frontend/src/components/Courses/CourseDetailTeacher.tsx +++ b/frontend/src/components/Courses/CourseDetailTeacher.tsx @@ -6,6 +6,7 @@ import { Link, useNavigate, NavigateFunction, useLoaderData } from "react-router import { Title } from "../Header/Title"; import ClearIcon from '@mui/icons-material/Clear'; import { timeDifference } from "../../utils/date-utils"; +import { get_csrf_cookie } from "../../utils/csrf"; interface UserUid{ uid: string @@ -22,7 +23,8 @@ function handleDeleteAdmin(navigate: NavigateFunction, courseId: string, uid: st method: 'DELETE', credentials: 'include', headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify({ "admin_uid": uid @@ -44,7 +46,8 @@ function handleDeleteStudent(navigate: NavigateFunction, courseId: string, uids: method: 'DELETE', credentials: 'include', headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify({ "students": uids @@ -64,6 +67,9 @@ function handleDeleteCourse(navigate: NavigateFunction, courseId: string): void fetch(`${apiHost}/courses/${courseId}`, { method: 'DELETE', credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }).then((response) => { if(response.ok){ navigate(-1); @@ -292,6 +298,9 @@ function JoinCodeMenu({courseId,open,handleClose, anchorEl}: {courseId:string, o fetch(`${apiHost}/courses/${courseId}/join_codes`, { method: 'GET', credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) .then(response => response.json()) .then(data => { @@ -321,7 +330,8 @@ function JoinCodeMenu({courseId,open,handleClose, anchorEl}: {courseId:string, o method: 'POST', credentials: 'include', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify(bodyContent) }) @@ -334,7 +344,8 @@ function JoinCodeMenu({courseId,open,handleClose, anchorEl}: {courseId:string, o method: 'DELETE', credentials: 'include', headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify({ "join_code": joinCode diff --git a/frontend/src/components/Courses/CourseUtilComponents.tsx b/frontend/src/components/Courses/CourseUtilComponents.tsx index 6a9e4804..e77a3634 100644 --- a/frontend/src/components/Courses/CourseUtilComponents.tsx +++ b/frontend/src/components/Courses/CourseUtilComponents.tsx @@ -4,6 +4,7 @@ import { Link, useNavigate, useLocation } from "react-router-dom"; import { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import debounce from 'debounce'; +import { get_csrf_cookie } from "../../utils/csrf"; /** * @param text - The text to be displayed @@ -100,7 +101,12 @@ export function SideScrollableCourses({courses}: {courses: Course[]}): JSX.Eleme const fetchProjects = async () => { const projectPromises = courses.map(course => fetch(`${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}`, - { credentials: 'include' } + { + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, + } ) .then(response => response.json()) ); diff --git a/frontend/src/components/Courses/CourseUtils.tsx b/frontend/src/components/Courses/CourseUtils.tsx index 51a462a2..4ecad4fa 100644 --- a/frontend/src/components/Courses/CourseUtils.tsx +++ b/frontend/src/components/Courses/CourseUtils.tsx @@ -1,4 +1,5 @@ import { NavigateFunction, Params } from 'react-router-dom'; +import { get_csrf_cookie } from '../../utils/csrf'; export interface Course{ course_id: string, @@ -48,7 +49,8 @@ export function callToApiToCreateCourse(data: string, navigate: NavigateFunction fetch(`${apiHost}/courses`, { credentials: 'include', // include, *same-origin, omit headers: { - "Content-Type": "application/json" + "Content-Type": "application/json", + "X-CSRF-TOKEN": get_csrf_cookie() }, method: 'POST', body: data, @@ -60,7 +62,8 @@ export function callToApiToCreateCourse(data: string, navigate: NavigateFunction credentials: 'include', method: 'POST', headers: { - 'Content-Type': 'application/json' + 'Content-Type': 'application/json', + "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify({admin_uid: loggedInUid()}) }); diff --git a/frontend/src/components/ProjectForm/ProjectForm.tsx b/frontend/src/components/ProjectForm/ProjectForm.tsx index e47f5354..d4c63b29 100644 --- a/frontend/src/components/ProjectForm/ProjectForm.tsx +++ b/frontend/src/components/ProjectForm/ProjectForm.tsx @@ -29,6 +29,7 @@ import {TabContext} from "@mui/lab"; import FileStuctureForm from "./FileStructureForm.tsx"; import AdvancedRegex from "./AdvancedRegex.tsx"; import RunnerSelecter from "./RunnerSelecter.tsx"; +import { get_csrf_cookie } from "../../utils/csrf.ts"; interface Course { course_id: string; @@ -137,7 +138,10 @@ export default function ProjectForm() { const fetchCourses = async () => { const response = await fetch(`${apiUrl}/courses?teacher=${user}`, { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) const jsonData = await response.json(); if (jsonData.data) { @@ -205,7 +209,10 @@ export default function ProjectForm() { const response = await fetch(`${apiUrl}/projects`, { method: "post", credentials: 'include', - body: formData + body: formData, + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) if (!response.ok) { diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx index f0937995..2aa91369 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx @@ -4,6 +4,7 @@ import {useParams} from "react-router-dom"; import ProjectSubmissionsOverviewDatagrid from "./ProjectSubmissionOverviewDatagrid.tsx"; import download from 'downloadjs'; import {useTranslation} from "react-i18next"; +import { get_csrf_cookie } from "../../utils/csrf.ts"; const apiUrl = import.meta.env.VITE_API_HOST /** @@ -19,7 +20,10 @@ export default function ProjectSubmissionOverview() { const fetchProject = async () => { const response = await fetch(`${apiUrl}/projects/${projectId}`, { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) const jsonData = await response.json(); setProjectTitle(jsonData["data"].title); @@ -29,6 +33,9 @@ export default function ProjectSubmissionOverview() { const downloadProjectSubmissions = async () => { await fetch(`${apiUrl}/projects/${projectId}/submissions-download`, { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) .then(res => { return res.blob(); diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx index 0c5fb167..42493a31 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx @@ -30,6 +30,9 @@ function getRowId(row: Submission) { const fetchSubmissionsFromUser = async (submission_id: string) => { await fetch(`${apiUrl}/submissions/${submission_id}/download`, { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) .then(res => { return res.blob(); @@ -85,6 +88,9 @@ export default function ProjectSubmissionsOverviewDatagrid() { const fetchLastSubmissionsByUser = async () => { const response = await fetch(`${apiUrl}/projects/${projectId}/latest-per-user`, { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) const jsonData = await response.json(); setSubmissions(jsonData.data); diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 81444303..2b6e08d3 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -1,3 +1,4 @@ +import { get_csrf_cookie } from "../../utils/csrf.ts"; import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx"; const API_URL = import.meta.env.VITE_APP_API_HOST @@ -10,7 +11,10 @@ export const fetchProjectPage = async () => { export const fetchMe = async () => { try { const response = await fetch(`${API_URL}/me`, { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) if(response.status == 200){ const data = await response.json() @@ -27,7 +31,10 @@ export const fetchProjects = async () => { try{ const response = await fetch(`${API_URL}/projects`, { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }) const jsonData = await response.json(); @@ -36,7 +43,10 @@ export const fetchProjects = async () => { const url_split = item.project_id.split('/') const project_id = url_split[url_split.length -1] const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?project_id=${project_id}`), { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + } })).json() @@ -49,12 +59,18 @@ export const fetchProjects = async () => { } )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { credentials: 'include' + const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, })).json() //fetch the course const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, })).json() const course = { course_id: response_courses.data.course_id, diff --git a/frontend/src/pages/project/projectView/ProjectView.tsx b/frontend/src/pages/project/projectView/ProjectView.tsx index 65053f4d..f886ed28 100644 --- a/frontend/src/pages/project/projectView/ProjectView.tsx +++ b/frontend/src/pages/project/projectView/ProjectView.tsx @@ -14,6 +14,7 @@ import { useParams } from "react-router-dom"; import SubmissionCard from "./SubmissionCard"; import { Course } from "../../../types/course"; import { Title } from "../../../components/Header/Title"; +import { get_csrf_cookie } from "../../../utils/csrf"; const API_URL = import.meta.env.VITE_API_HOST; @@ -36,6 +37,9 @@ export default function ProjectView() { useEffect(() => { fetch(`${API_URL}/projects/${projectId}`, { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }).then((response) => { if (response.ok) { response.json().then((data) => { @@ -43,6 +47,9 @@ export default function ProjectView() { setProjectData(projectData); fetch(`${API_URL}/courses/${projectData.course_id}`, { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }).then((response) => { if (response.ok) { response.json().then((data) => { @@ -56,6 +63,9 @@ export default function ProjectView() { fetch(`${API_URL}/projects/${projectId}/assignment`, { credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }).then((response) => { if (response.ok) { response.text().then((data) => setAssignmentRawText(data)); diff --git a/frontend/src/pages/project/projectView/SubmissionCard.tsx b/frontend/src/pages/project/projectView/SubmissionCard.tsx index 84016e54..d22b64db 100644 --- a/frontend/src/pages/project/projectView/SubmissionCard.tsx +++ b/frontend/src/pages/project/projectView/SubmissionCard.tsx @@ -16,6 +16,7 @@ import axios from "axios"; import { useTranslation } from "react-i18next"; import SubmissionsGrid from "./SubmissionsGrid"; import { Submission } from "../../../types/submission"; +import { get_csrf_cookie } from "../../../utils/csrf"; interface SubmissionCardProps { regexRequirements?: string[]; @@ -48,7 +49,10 @@ export default function SubmissionCard({ useEffect(() => { fetch(`${submissionUrl}?project_id=${projectId}`, { - credentials: 'include' + credentials: 'include', + headers: { + "X-CSRF-TOKEN": get_csrf_cookie() + }, }).then((response) => { if (response.ok) { response.json().then((data) => { From 23b7be5e1c4b2287c0103e40543c8ba9a79b2a54 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Thu, 25 Apr 2024 15:01:45 +0200 Subject: [PATCH 5/9] forgot an import --- .../ProjectSubmissionOverviewDatagrid.tsx | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx index 42493a31..afc9665e 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx @@ -7,6 +7,7 @@ import { green, red } from '@mui/material/colors'; import CancelIcon from '@mui/icons-material/Cancel'; import DownloadIcon from '@mui/icons-material/Download'; import download from "downloadjs"; +import { get_csrf_cookie } from "../../utils/csrf"; const apiUrl = import.meta.env.VITE_API_HOST From 85db2ef1ee6c1a1b7e5b1a5e586a8abdde2bd561 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Thu, 25 Apr 2024 15:11:58 +0200 Subject: [PATCH 6/9] formatting --- .../src/components/Courses/CourseUtils.tsx | 89 +++---- frontend/src/pages/project/FetchProjects.tsx | 220 ++++++++++-------- 2 files changed, 171 insertions(+), 138 deletions(-) diff --git a/frontend/src/components/Courses/CourseUtils.tsx b/frontend/src/components/Courses/CourseUtils.tsx index 4ecad4fa..4ca30441 100644 --- a/frontend/src/components/Courses/CourseUtils.tsx +++ b/frontend/src/components/Courses/CourseUtils.tsx @@ -1,18 +1,18 @@ -import { NavigateFunction, Params } from 'react-router-dom'; -import { get_csrf_cookie } from '../../utils/csrf'; - -export interface Course{ - course_id: string, - name: string, - teacher:string, - ufora_id:string, - url:string +import { NavigateFunction, Params } from "react-router-dom"; +import { get_csrf_cookie } from "../../utils/csrf"; + +export interface Course { + course_id: string; + name: string; + teacher: string; + ufora_id: string; + url: string; } -export interface Project{ - title: string, - project_id: string, - deadlines: string[][] +export interface Project { + title: string; + project_id: string; + deadlines: string[][]; } export const apiHost = import.meta.env.VITE_APP_API_HOST; @@ -20,7 +20,7 @@ export const appHost = import.meta.env.VITE_APP_HOST; /** * @returns The uid of the acces token of the logged in user */ -export function loggedInToken(){ +export function loggedInToken() { return "teacher1"; } @@ -36,7 +36,7 @@ export function getUserName(uid: string): string { /** * @returns The Uid of the logged in user */ -export function loggedInUid(){ +export function loggedInUid() { return "Gunnar"; } @@ -45,38 +45,41 @@ export function loggedInUid(){ * @param data - course data to send to the api * @param navigate - function that allows the app to redirect */ -export function callToApiToCreateCourse(data: string, navigate: NavigateFunction){ +export function callToApiToCreateCourse( + data: string, + navigate: NavigateFunction +) { fetch(`${apiHost}/courses`, { - credentials: 'include', // include, *same-origin, omit + credentials: "include", // include, *same-origin, omit headers: { "Content-Type": "application/json", - "X-CSRF-TOKEN": get_csrf_cookie() + "X-CSRF-TOKEN": get_csrf_cookie(), }, - method: 'POST', + method: "POST", body: data, }) - .then(response => response.json()) - .then(data => { + .then((response) => response.json()) + .then((data) => { //But first also make sure that teacher is in the course admins list fetch(`${apiHost}/courses/${getIdFromLink(data.url)}/admins`, { - credentials: 'include', - method: 'POST', + credentials: "include", + method: "POST", headers: { - 'Content-Type': 'application/json', - "X-CSRF-TOKEN": get_csrf_cookie() + "Content-Type": "application/json", + "X-CSRF-TOKEN": get_csrf_cookie(), }, - body: JSON.stringify({admin_uid: loggedInUid()}) + body: JSON.stringify({ admin_uid: loggedInUid() }), }); navigate(getIdFromLink(data.url)); // navigate to data.url - }) + }); } - + /** * @param link - the link to the api endpoint * @returns the Id at the end of the link */ export function getIdFromLink(link: string): string { - const parts = link.split('/'); + const parts = link.split("/"); return parts[parts.length - 1]; } @@ -87,9 +90,13 @@ export function getIdFromLink(link: string): string { */ export function getNearestFutureDate(dates: string[][]): Date | null { const now = new Date(); - const futureDates = dates.map(date => new Date(date[1])).filter(date => date > now); + const futureDates = dates + .map((date) => new Date(date[1])) + .filter((date) => date > now); if (futureDates.length === 0) return null; - return futureDates.reduce((nearest, current) => current < nearest ? current : nearest); + return futureDates.reduce((nearest, current) => + current < nearest ? current : nearest + ); } /** @@ -99,14 +106,14 @@ export function getNearestFutureDate(dates: string[][]): Date | null { const fetchData = async (url: string, params?: URLSearchParams) => { let uri = `${apiHost}/${url}`; - if(params){ - uri += `?${params}` + if (params) { + uri += `?${params}`; } const res = await fetch(uri, { - credentials: 'include' + credentials: "include", }); - if(res.status !== 200){ - throw new Response("Failed to fetch data", {status: res.status}); + if (res.status !== 200) { + throw new Response("Failed to fetch data", { status: res.status }); } const jsonResult = await res.json(); @@ -135,7 +142,11 @@ const dataLoaderStudents = async (courseId: string) => { return fetchData(`courses/${courseId}/students`); }; -export const dataLoaderCourseDetail = async ({ params } : { params:Params}) => { +export const dataLoaderCourseDetail = async ({ + params, +}: { + params: Params; +}) => { const { courseId } = params; if (!courseId) { throw new Error("Course ID is undefined."); @@ -144,6 +155,6 @@ export const dataLoaderCourseDetail = async ({ params } : { params:Params}) => { const projects = await dataLoaderProjects(courseId); const admins = await dataLoaderAdmins(courseId); const students = await dataLoaderStudents(courseId); - + return { course, projects, admins, students }; -}; \ No newline at end of file +}; diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 2b6e08d3..5938321f 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -1,130 +1,152 @@ import { get_csrf_cookie } from "../../utils/csrf.ts"; -import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx"; -const API_URL = import.meta.env.VITE_APP_API_HOST +import { + Project, + ProjectDeadline, + ShortSubmission, +} from "./projectDeadline/ProjectDeadline.tsx"; +const API_URL = import.meta.env.VITE_APP_API_HOST; export const fetchProjectPage = async () => { - const projects = await fetchProjects() - const me = await fetchMe() - return {projects, me} -} + const projects = await fetchProjects(); + const me = await fetchMe(); + return { projects, me }; +}; export const fetchMe = async () => { try { const response = await fetch(`${API_URL}/me`, { - credentials: 'include', + credentials: "include", headers: { - "X-CSRF-TOKEN": get_csrf_cookie() + "X-CSRF-TOKEN": get_csrf_cookie(), }, - }) - if(response.status == 200){ - const data = await response.json() - return data.role - }else { - return "UNKNOWN" + }); + if (response.status == 200) { + const data = await response.json(); + return data.role; + } else { + return "UNKNOWN"; } - } catch (e){ - return "UNKNOWN" + } catch (e) { + return "UNKNOWN"; } - -} +}; export const fetchProjects = async () => { - - try{ + try { const response = await fetch(`${API_URL}/projects`, { - credentials: 'include', + credentials: "include", headers: { - "X-CSRF-TOKEN": get_csrf_cookie() + "X-CSRF-TOKEN": get_csrf_cookie(), }, - - }) + }); const jsonData = await response.json(); - let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item:Project) => { - try{ - const url_split = item.project_id.split('/') - const project_id = url_split[url_split.length -1] - const response_submissions = await (await fetch(encodeURI(`${API_URL}/submissions?project_id=${project_id}`), { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - } + let formattedData: ProjectDeadline[] = await Promise.all( + jsonData.data.map(async (item: Project) => { + try { + const url_split = item.project_id.split("/"); + const project_id = url_split[url_split.length - 1]; + const response_submissions = await ( + await fetch( + encodeURI(`${API_URL}/submissions?project_id=${project_id}`), + { + credentials: "include", + headers: { + "X-CSRF-TOKEN": get_csrf_cookie(), + }, + } + ) + ).json(); - })).json() + //get the latest submission + const latest_submission = response_submissions.data + .map((submission: ShortSubmission) => ({ + submission_id: submission.submission_id, //this is the path + submission_time: new Date(submission.submission_time), + submission_status: submission.submission_status, + grading: submission.grading, + })) + .sort( + (a: ShortSubmission, b: ShortSubmission) => + b.submission_time.getTime() - a.submission_time.getTime() + )[0]; + // fetch the course id of the project + const project_item = await ( + await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { + credentials: "include", + headers: { + "X-CSRF-TOKEN": get_csrf_cookie(), + }, + }) + ).json(); - //get the latest submission - const latest_submission = response_submissions.data.map((submission:ShortSubmission) => ({ - submission_id: submission.submission_id,//this is the path - submission_time: new Date(submission.submission_time), - submission_status: submission.submission_status, - grading: submission.grading - } - )).sort((a:ShortSubmission, b:ShortSubmission) => b.submission_time.getTime() - a.submission_time.getTime())[0]; - // fetch the course id of the project - const project_item = await (await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - })).json() - - //fetch the course - const response_courses = await (await fetch(encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - })).json() - const course = { - course_id: response_courses.data.course_id, - name: response_courses.data.name, - teacher: response_courses.data.teacher, - ufora_id: response_courses.data.ufora_id - } - if(project_item.data.deadlines){ - return project_item.data.deadlines.map((d:string[]) => { - return { + //fetch the course + const response_courses = await ( + await fetch( + encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), + { + credentials: "include", + headers: { + "X-CSRF-TOKEN": get_csrf_cookie(), + }, + } + ) + ).json(); + const course = { + course_id: response_courses.data.course_id, + name: response_courses.data.name, + teacher: response_courses.data.teacher, + ufora_id: response_courses.data.ufora_id, + }; + if (project_item.data.deadlines) { + return project_item.data.deadlines.map((d: string[]) => { + return { + project_id: project_id, + title: project_item.data.title, + description: project_item.data.description, + assignment_file: project_item.data.assignment_file, + deadline: new Date(d[1]), + deadline_description: d[0], + course_id: Number(project_item.data.course_id), + visible_for_students: Boolean( + project_item.data.visible_for_students + ), + archived: Boolean(project_item.data.archived), + test_path: project_item.data.test_path, + script_name: project_item.data.script_name, + regex_expressions: project_item.data.regex_expressions, + short_submission: latest_submission, + course: course, + }; + }); + } + // contains no dealine: + return [ + { project_id: project_id, title: project_item.data.title, description: project_item.data.description, assignment_file: project_item.data.assignment_file, - deadline: new Date(d[1]), - deadline_description: d[0], + deadline: undefined, + deadline_description: undefined, course_id: Number(project_item.data.course_id), - visible_for_students: Boolean(project_item.data.visible_for_students), + visible_for_students: Boolean( + project_item.data.visible_for_students + ), archived: Boolean(project_item.data.archived), test_path: project_item.data.test_path, script_name: project_item.data.script_name, regex_expressions: project_item.data.regex_expressions, short_submission: latest_submission, - course: course - } - }) + course: course, + }, + ]; + } catch (e) { + return []; } - // contains no dealine: - return [{ - project_id: project_id, - title: project_item.data.title, - description: project_item.data.description, - assignment_file: project_item.data.assignment_file, - deadline: undefined, - deadline_description: undefined, - course_id: Number(project_item.data.course_id), - visible_for_students: Boolean(project_item.data.visible_for_students), - archived: Boolean(project_item.data.archived), - test_path: project_item.data.test_path, - script_name: project_item.data.script_name, - regex_expressions: project_item.data.regex_expressions, - short_submission: latest_submission, - course: course - }] - - }catch (e){ - return [] - } - } - - )); - formattedData = formattedData.flat() - return formattedData + }) + ); + formattedData = formattedData.flat(); + return formattedData; } catch (e) { - return [] + return []; } -} +}; From b397cbcc71d30b0de794f769a95ebca510e12923 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Thu, 25 Apr 2024 15:17:08 +0200 Subject: [PATCH 7/9] formatting --- .../Courses/CourseUtilComponents.tsx | 278 ++++++++++++------ 1 file changed, 196 insertions(+), 82 deletions(-) diff --git a/frontend/src/components/Courses/CourseUtilComponents.tsx b/frontend/src/components/Courses/CourseUtilComponents.tsx index e77a3634..372e4d8b 100644 --- a/frontend/src/components/Courses/CourseUtilComponents.tsx +++ b/frontend/src/components/Courses/CourseUtilComponents.tsx @@ -1,18 +1,45 @@ -import { Box, Button, Card, CardActions, CardContent, CardHeader, Grid, Paper, TextField, Typography } from "@mui/material"; -import { Course, Project, apiHost, getIdFromLink, getNearestFutureDate } from "./CourseUtils"; +import { + Box, + Button, + Card, + CardActions, + CardContent, + CardHeader, + Grid, + Paper, + TextField, + Typography, +} from "@mui/material"; +import { + Course, + Project, + apiHost, + getIdFromLink, + getNearestFutureDate, +} from "./CourseUtils"; import { Link, useNavigate, useLocation } from "react-router-dom"; import { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; -import debounce from 'debounce'; +import debounce from "debounce"; import { get_csrf_cookie } from "../../utils/csrf"; /** * @param text - The text to be displayed * @returns Typography that overflow into ... when text is too long */ -export function EpsilonTypography({text} : {text: string}): JSX.Element { +export function EpsilonTypography({ text }: { text: string }): JSX.Element { return ( - {text} + + {text} + ); } @@ -22,7 +49,15 @@ export function EpsilonTypography({text} : {text: string}): JSX.Element { * @param handleSearchChange - The function to handle search term changes * @returns a Grid item containing a TextField, used for searching/filtering */ -export function SearchBox({label,searchTerm,handleSearchChange}: {label: string, searchTerm: string, handleSearchChange: (event: React.ChangeEvent) => void}): JSX.Element { +export function SearchBox({ + label, + searchTerm, + handleSearchChange, +}: { + label: string; + searchTerm: string; + handleSearchChange: (event: React.ChangeEvent) => void; +}): JSX.Element { return ( @@ -42,43 +77,57 @@ export function SearchBox({label,searchTerm,handleSearchChange}: {label: string, * @param props - The component props requiring the courses that will be displayed in the scroller. * @returns A component to display courses in horizontal scroller where each course is a card containing its name. */ -export function SideScrollableCourses({courses}: {courses: Course[]}): JSX.Element { +export function SideScrollableCourses({ + courses, +}: { + courses: Course[]; +}): JSX.Element { //const navigate = useNavigate(); const location = useLocation(); const navigate = useNavigate(); // Get initial state from URL - const urlParams = useMemo(() => new URLSearchParams(location.search), [location.search]); //useMemo so only recompute when location.search changes - const initialSearchTerm = urlParams.get('name') || ''; - const initialUforaIdFilter = urlParams.get('ufora_id') || ''; - const initialTeacherNameFilter = urlParams.get('teacher') || ''; + const urlParams = useMemo( + () => new URLSearchParams(location.search), + [location.search] + ); //useMemo so only recompute when location.search changes + const initialSearchTerm = urlParams.get("name") || ""; + const initialUforaIdFilter = urlParams.get("ufora_id") || ""; + const initialTeacherNameFilter = urlParams.get("teacher") || ""; const [searchTerm, setSearchTerm] = useState(initialSearchTerm); const [uforaIdFilter, setUforaIdFilter] = useState(initialUforaIdFilter); - const [teacherNameFilter, setTeacherNameFilter] = useState(initialTeacherNameFilter); - const [projects, setProjects] = useState<{ [courseId: string]: Project[] }>({}); - - const debouncedHandleSearchChange = useMemo(() => - debounce((key: string, value: string) => { - if (value === '') { - urlParams.delete(key); - } else { - urlParams.set(key, value); - } - const newUrl = `${location.pathname}?${urlParams.toString()}`; - navigate(newUrl, { replace: true }); - }, 500), [urlParams, navigate, location.pathname]); + const [teacherNameFilter, setTeacherNameFilter] = useState( + initialTeacherNameFilter + ); + const [projects, setProjects] = useState<{ [courseId: string]: Project[] }>( + {} + ); + + const debouncedHandleSearchChange = useMemo( + () => + debounce((key: string, value: string) => { + if (value === "") { + urlParams.delete(key); + } else { + urlParams.set(key, value); + } + const newUrl = `${location.pathname}?${urlParams.toString()}`; + navigate(newUrl, { replace: true }); + }, 500), + [urlParams, navigate, location.pathname] + ); useEffect(() => { - debouncedHandleSearchChange('name', searchTerm); + debouncedHandleSearchChange("name", searchTerm); }, [searchTerm, debouncedHandleSearchChange]); useEffect(() => { - debouncedHandleSearchChange('ufora_id', uforaIdFilter); + debouncedHandleSearchChange("ufora_id", uforaIdFilter); }, [uforaIdFilter, debouncedHandleSearchChange]); useEffect(() => { - debouncedHandleSearchChange('teacher', teacherNameFilter); + debouncedHandleSearchChange("teacher", teacherNameFilter); }, [teacherNameFilter, debouncedHandleSearchChange]); const handleSearchChange = (event: React.ChangeEvent) => { @@ -86,12 +135,16 @@ export function SideScrollableCourses({courses}: {courses: Course[]}): JSX.Eleme setSearchTerm(newSearchTerm); }; - const handleUforaIdFilterChange = (event: React.ChangeEvent) => { + const handleUforaIdFilterChange = ( + event: React.ChangeEvent + ) => { const newUforaIdFilter = event.target.value; setUforaIdFilter(newUforaIdFilter); }; - const handleTeacherNameFilterChange = (event: React.ChangeEvent) => { + const handleTeacherNameFilterChange = ( + event: React.ChangeEvent + ) => { const newTeacherNameFilter = event.target.value; setTeacherNameFilter(newTeacherNameFilter); }; @@ -99,16 +152,16 @@ export function SideScrollableCourses({courses}: {courses: Course[]}): JSX.Eleme useEffect(() => { // Fetch projects for each course const fetchProjects = async () => { - const projectPromises = courses.map(course => - fetch(`${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}`, - { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - } - ) - .then(response => response.json()) + const projectPromises = courses.map((course) => + fetch( + `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}`, + { + credentials: "include", + headers: { + "X-CSRF-TOKEN": get_csrf_cookie(), + }, + } + ).then((response) => response.json()) ); const projectResults = await Promise.all(projectPromises); @@ -124,59 +177,105 @@ export function SideScrollableCourses({courses}: {courses: Course[]}): JSX.Eleme fetchProjects(); }, [courses]); - const filteredCourses = courses.filter(course => - course.name.toLowerCase().includes(searchTerm.toLowerCase()) && - (course.ufora_id ? course.ufora_id.toLowerCase().includes(uforaIdFilter.toLowerCase()) : !uforaIdFilter) && - course.teacher.toLowerCase().includes(teacherNameFilter.toLowerCase()) + const filteredCourses = courses.filter( + (course) => + course.name.toLowerCase().includes(searchTerm.toLowerCase()) && + (course.ufora_id + ? course.ufora_id.toLowerCase().includes(uforaIdFilter.toLowerCase()) + : !uforaIdFilter) && + course.teacher.toLowerCase().includes(teacherNameFilter.toLowerCase()) ); return ( - - - + + + - + ); } /** - * Empty or not. + * Empty or not. * @returns either a place holder or the actual content. */ -function EmptyOrNotFilteredCourses({filteredCourses, projects}: {filteredCourses: Course[], projects: { [courseId: string]: Project[] }}): JSX.Element { - const { t } = useTranslation('translation', { keyPrefix: 'courseDetailTeacher' }); - if(filteredCourses.length === 0){ +function EmptyOrNotFilteredCourses({ + filteredCourses, + projects, +}: { + filteredCourses: Course[]; + projects: { [courseId: string]: Project[] }; +}): JSX.Element { + const { t } = useTranslation("translation", { + keyPrefix: "courseDetailTeacher", + }); + if (filteredCourses.length === 0) { return ( - {t('noCoursesFound')} + + {t("noCoursesFound")} + ); } return ( - + {filteredCourses.map((course, index) => ( - - }/> - - {course.ufora_id && ( - <> - Ufora_id: {course.ufora_id}
- - )} - Teacher: {course.teacher} - - }/> - - {t('projects')}: - + + } /> + + {course.ufora_id && ( + <> + Ufora_id: {course.ufora_id} +
+ + )} + Teacher: {course.teacher} + + } + /> + + {t("projects")}: + - + + +
@@ -186,36 +285,51 @@ function EmptyOrNotFilteredCourses({filteredCourses, projects}: {filteredCourses ); } /** - * @param projects - The projects to be displayed if not empty + * @param projects - The projects to be displayed if not empty * @returns either a place holder with text for no projects or the projects */ -function EmptyOrNotProjects({projects, noProjectsText}: {projects: Project[], noProjectsText:string}): JSX.Element { - if(projects === undefined || projects.length === 0){ +function EmptyOrNotProjects({ + projects, + noProjectsText, +}: { + projects: Project[]; + noProjectsText: string; +}): JSX.Element { + if (projects === undefined || projects.length === 0) { return ( - {noProjectsText} + + {noProjectsText} + ); - } - else{ + } else { const now = new Date(); return ( <> {projects.slice(0, 3).map((project) => { - let timeLeft = ''; + let timeLeft = ""; if (project.deadlines != undefined) { const deadlineDate = getNearestFutureDate(project.deadlines); - if(deadlineDate == null){ - return <> + if (deadlineDate == null) { + return <>; } const diffTime = Math.abs(deadlineDate.getTime() - now.getTime()); const diffHours = Math.ceil(diffTime / (1000 * 60 * 60)); const diffDays = Math.ceil(diffHours * 24); - + timeLeft = diffDays > 1 ? `${diffDays} days` : `${diffHours} hours`; } return ( - - + + ); @@ -223,4 +337,4 @@ function EmptyOrNotProjects({projects, noProjectsText}: {projects: Project[], no ); } -} \ No newline at end of file +} From 9640d0fc099ae7f82ebdd8cf5ef24583b25724d7 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Thu, 25 Apr 2024 15:57:27 +0200 Subject: [PATCH 8/9] added custom fetch function --- .../Courses/CourseDetailTeacher.tsx | 33 +++----------- .../Courses/CourseUtilComponents.tsx | 12 ++---- .../src/components/Courses/CourseUtils.tsx | 14 ++---- .../components/ProjectForm/ProjectForm.tsx | 15 ++----- .../ProjectSubmissionOverview.tsx | 16 ++----- .../ProjectSubmissionOverviewDatagrid.tsx | 16 ++----- frontend/src/pages/project/FetchProjects.tsx | 43 ++++--------------- .../pages/project/projectView/ProjectView.tsx | 23 ++-------- .../project/projectView/SubmissionCard.tsx | 9 +--- frontend/src/utils/authenticated-fetch.ts | 17 ++++++++ frontend/src/utils/csrf.ts | 2 +- 11 files changed, 55 insertions(+), 145 deletions(-) create mode 100644 frontend/src/utils/authenticated-fetch.ts diff --git a/frontend/src/components/Courses/CourseDetailTeacher.tsx b/frontend/src/components/Courses/CourseDetailTeacher.tsx index 28d2819b..cd729766 100644 --- a/frontend/src/components/Courses/CourseDetailTeacher.tsx +++ b/frontend/src/components/Courses/CourseDetailTeacher.tsx @@ -6,7 +6,7 @@ import { Link, useNavigate, NavigateFunction, useLoaderData } from "react-router import { Title } from "../Header/Title"; import ClearIcon from '@mui/icons-material/Clear'; import { timeDifference } from "../../utils/date-utils"; -import { get_csrf_cookie } from "../../utils/csrf"; +import { authenticatedFetch } from "../../utils/authenticated-fetch"; interface UserUid{ uid: string @@ -19,13 +19,8 @@ interface UserUid{ * @param uid - The UID of the admin. */ function handleDeleteAdmin(navigate: NavigateFunction, courseId: string, uid: string): void { - fetch(`${apiHost}/courses/${courseId}/admins`, { + authenticatedFetch(`${apiHost}/courses/${courseId}/admins`, { method: 'DELETE', - credentials: 'include', - headers: { - "Content-Type": "application/json", - "X-CSRF-TOKEN": get_csrf_cookie() - }, body: JSON.stringify({ "admin_uid": uid }) @@ -42,12 +37,10 @@ function handleDeleteAdmin(navigate: NavigateFunction, courseId: string, uid: st * @param uid - The UID of the admin. */ function handleDeleteStudent(navigate: NavigateFunction, courseId: string, uids: string[]): void { - fetch(`${apiHost}/courses/${courseId}/students`, { + authenticatedFetch(`${apiHost}/courses/${courseId}/students`, { method: 'DELETE', - credentials: 'include', headers: { "Content-Type": "application/json", - "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify({ "students": uids @@ -64,12 +57,8 @@ function handleDeleteStudent(navigate: NavigateFunction, courseId: string, uids: * @param courseId - The ID of the course. */ function handleDeleteCourse(navigate: NavigateFunction, courseId: string): void { - fetch(`${apiHost}/courses/${courseId}`, { + authenticatedFetch(`${apiHost}/courses/${courseId}`, { method: 'DELETE', - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, }).then((response) => { if(response.ok){ navigate(-1); @@ -295,12 +284,8 @@ function JoinCodeMenu({courseId,open,handleClose, anchorEl}: {courseId:string, o }; const getCodes = useCallback(() => { - fetch(`${apiHost}/courses/${courseId}/join_codes`, { + authenticatedFetch(`${apiHost}/courses/${courseId}/join_codes`, { method: 'GET', - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, }) .then(response => response.json()) .then(data => { @@ -326,12 +311,10 @@ function JoinCodeMenu({courseId,open,handleClose, anchorEl}: {courseId:string, o bodyContent.expiry_time = expiry_time.toISOString(); } - fetch(`${apiHost}/courses/${courseId}/join_codes`, { + authenticatedFetch(`${apiHost}/courses/${courseId}/join_codes`, { method: 'POST', - credentials: 'include', headers: { 'Content-Type': 'application/json', - "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify(bodyContent) }) @@ -339,13 +322,11 @@ function JoinCodeMenu({courseId,open,handleClose, anchorEl}: {courseId:string, o } const handleDeleteCode = (joinCode: string) => { - fetch(`${apiHost}/courses/${courseId}/join_codes/${joinCode}`, + authenticatedFetch(`${apiHost}/courses/${courseId}/join_codes/${joinCode}`, { method: 'DELETE', - credentials: 'include', headers: { "Content-Type": "application/json", - "X-CSRF-TOKEN": get_csrf_cookie() }, body: JSON.stringify({ "join_code": joinCode diff --git a/frontend/src/components/Courses/CourseUtilComponents.tsx b/frontend/src/components/Courses/CourseUtilComponents.tsx index 372e4d8b..48d6bdb2 100644 --- a/frontend/src/components/Courses/CourseUtilComponents.tsx +++ b/frontend/src/components/Courses/CourseUtilComponents.tsx @@ -21,7 +21,7 @@ import { Link, useNavigate, useLocation } from "react-router-dom"; import { useState, useEffect, useMemo } from "react"; import { useTranslation } from "react-i18next"; import debounce from "debounce"; -import { get_csrf_cookie } from "../../utils/csrf"; +import { authenticatedFetch } from "../../utils/authenticated-fetch"; /** * @param text - The text to be displayed @@ -153,14 +153,8 @@ export function SideScrollableCourses({ // Fetch projects for each course const fetchProjects = async () => { const projectPromises = courses.map((course) => - fetch( - `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}`, - { - credentials: "include", - headers: { - "X-CSRF-TOKEN": get_csrf_cookie(), - }, - } + authenticatedFetch( + `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}` ).then((response) => response.json()) ); diff --git a/frontend/src/components/Courses/CourseUtils.tsx b/frontend/src/components/Courses/CourseUtils.tsx index 4ca30441..331b5ea5 100644 --- a/frontend/src/components/Courses/CourseUtils.tsx +++ b/frontend/src/components/Courses/CourseUtils.tsx @@ -1,5 +1,5 @@ import { NavigateFunction, Params } from "react-router-dom"; -import { get_csrf_cookie } from "../../utils/csrf"; +import { authenticatedFetch } from "../../utils/authenticated-fetch"; export interface Course { course_id: string; @@ -49,11 +49,9 @@ export function callToApiToCreateCourse( data: string, navigate: NavigateFunction ) { - fetch(`${apiHost}/courses`, { - credentials: "include", // include, *same-origin, omit + authenticatedFetch(`${apiHost}/courses`, { headers: { "Content-Type": "application/json", - "X-CSRF-TOKEN": get_csrf_cookie(), }, method: "POST", body: data, @@ -61,12 +59,10 @@ export function callToApiToCreateCourse( .then((response) => response.json()) .then((data) => { //But first also make sure that teacher is in the course admins list - fetch(`${apiHost}/courses/${getIdFromLink(data.url)}/admins`, { - credentials: "include", + authenticatedFetch(`${apiHost}/courses/${getIdFromLink(data.url)}/admins`, { method: "POST", headers: { "Content-Type": "application/json", - "X-CSRF-TOKEN": get_csrf_cookie(), }, body: JSON.stringify({ admin_uid: loggedInUid() }), }); @@ -109,9 +105,7 @@ const fetchData = async (url: string, params?: URLSearchParams) => { if (params) { uri += `?${params}`; } - const res = await fetch(uri, { - credentials: "include", - }); + const res = await authenticatedFetch(uri); if (res.status !== 200) { throw new Response("Failed to fetch data", { status: res.status }); } diff --git a/frontend/src/components/ProjectForm/ProjectForm.tsx b/frontend/src/components/ProjectForm/ProjectForm.tsx index d4c63b29..b9410bc5 100644 --- a/frontend/src/components/ProjectForm/ProjectForm.tsx +++ b/frontend/src/components/ProjectForm/ProjectForm.tsx @@ -29,7 +29,7 @@ import {TabContext} from "@mui/lab"; import FileStuctureForm from "./FileStructureForm.tsx"; import AdvancedRegex from "./AdvancedRegex.tsx"; import RunnerSelecter from "./RunnerSelecter.tsx"; -import { get_csrf_cookie } from "../../utils/csrf.ts"; +import { authenticatedFetch } from "../../utils/authenticated-fetch.ts"; interface Course { course_id: string; @@ -137,12 +137,7 @@ export default function ProjectForm() { } const fetchCourses = async () => { - const response = await fetch(`${apiUrl}/courses?teacher=${user}`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }) + const response = await authenticatedFetch(`${apiUrl}/courses?teacher=${user}`) const jsonData = await response.json(); if (jsonData.data) { setCourses(jsonData.data); @@ -206,13 +201,9 @@ export default function ProjectForm() { formData.append("runner", runner); } - const response = await fetch(`${apiUrl}/projects`, { + const response = await authenticatedFetch(`${apiUrl}/projects`, { method: "post", - credentials: 'include', body: formData, - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, }) if (!response.ok) { diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx index 2aa91369..2e26bb07 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx @@ -4,7 +4,7 @@ import {useParams} from "react-router-dom"; import ProjectSubmissionsOverviewDatagrid from "./ProjectSubmissionOverviewDatagrid.tsx"; import download from 'downloadjs'; import {useTranslation} from "react-i18next"; -import { get_csrf_cookie } from "../../utils/csrf.ts"; +import { authenticatedFetch } from "../../utils/authenticated-fetch.ts"; const apiUrl = import.meta.env.VITE_API_HOST /** @@ -19,24 +19,14 @@ export default function ProjectSubmissionOverview() { }); const fetchProject = async () => { - const response = await fetch(`${apiUrl}/projects/${projectId}`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }) + const response = await authenticatedFetch(`${apiUrl}/projects/${projectId}`) const jsonData = await response.json(); setProjectTitle(jsonData["data"].title); } const downloadProjectSubmissions = async () => { - await fetch(`${apiUrl}/projects/${projectId}/submissions-download`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }) + await authenticatedFetch(`${apiUrl}/projects/${projectId}/submissions-download`) .then(res => { return res.blob(); }) diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx index afc9665e..a617d47e 100644 --- a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx +++ b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverviewDatagrid.tsx @@ -7,7 +7,7 @@ import { green, red } from '@mui/material/colors'; import CancelIcon from '@mui/icons-material/Cancel'; import DownloadIcon from '@mui/icons-material/Download'; import download from "downloadjs"; -import { get_csrf_cookie } from "../../utils/csrf"; +import { authenticatedFetch } from "../../utils/authenticated-fetch"; const apiUrl = import.meta.env.VITE_API_HOST @@ -29,12 +29,7 @@ function getRowId(row: Submission) { } const fetchSubmissionsFromUser = async (submission_id: string) => { - await fetch(`${apiUrl}/submissions/${submission_id}/download`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }) + await authenticatedFetch(`${apiUrl}/submissions/${submission_id}/download`) .then(res => { return res.blob(); }) @@ -87,12 +82,7 @@ export default function ProjectSubmissionsOverviewDatagrid() { }); const fetchLastSubmissionsByUser = async () => { - const response = await fetch(`${apiUrl}/projects/${projectId}/latest-per-user`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }) + const response = await authenticatedFetch(`${apiUrl}/projects/${projectId}/latest-per-user`) const jsonData = await response.json(); setSubmissions(jsonData.data); } diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 5938321f..64194a99 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -1,4 +1,4 @@ -import { get_csrf_cookie } from "../../utils/csrf.ts"; +import { authenticatedFetch } from "../../utils/authenticated-fetch.ts"; import { Project, ProjectDeadline, @@ -14,12 +14,7 @@ export const fetchProjectPage = async () => { export const fetchMe = async () => { try { - const response = await fetch(`${API_URL}/me`, { - credentials: "include", - headers: { - "X-CSRF-TOKEN": get_csrf_cookie(), - }, - }); + const response = await authenticatedFetch(`${API_URL}/me`); if (response.status == 200) { const data = await response.json(); return data.role; @@ -32,12 +27,7 @@ export const fetchMe = async () => { }; export const fetchProjects = async () => { try { - const response = await fetch(`${API_URL}/projects`, { - credentials: "include", - headers: { - "X-CSRF-TOKEN": get_csrf_cookie(), - }, - }); + const response = await authenticatedFetch(`${API_URL}/projects`); const jsonData = await response.json(); let formattedData: ProjectDeadline[] = await Promise.all( jsonData.data.map(async (item: Project) => { @@ -45,14 +35,8 @@ export const fetchProjects = async () => { const url_split = item.project_id.split("/"); const project_id = url_split[url_split.length - 1]; const response_submissions = await ( - await fetch( - encodeURI(`${API_URL}/submissions?project_id=${project_id}`), - { - credentials: "include", - headers: { - "X-CSRF-TOKEN": get_csrf_cookie(), - }, - } + await authenticatedFetch( + encodeURI(`${API_URL}/submissions?project_id=${project_id}`) ) ).json(); @@ -70,24 +54,13 @@ export const fetchProjects = async () => { )[0]; // fetch the course id of the project const project_item = await ( - await fetch(encodeURI(`${API_URL}/projects/${project_id}`), { - credentials: "include", - headers: { - "X-CSRF-TOKEN": get_csrf_cookie(), - }, - }) + await authenticatedFetch(encodeURI(`${API_URL}/projects/${project_id}`)) ).json(); //fetch the course const response_courses = await ( - await fetch( - encodeURI(`${API_URL}/courses/${project_item.data.course_id}`), - { - credentials: "include", - headers: { - "X-CSRF-TOKEN": get_csrf_cookie(), - }, - } + await authenticatedFetch( + encodeURI(`${API_URL}/courses/${project_item.data.course_id}`) ) ).json(); const course = { diff --git a/frontend/src/pages/project/projectView/ProjectView.tsx b/frontend/src/pages/project/projectView/ProjectView.tsx index f886ed28..80335194 100644 --- a/frontend/src/pages/project/projectView/ProjectView.tsx +++ b/frontend/src/pages/project/projectView/ProjectView.tsx @@ -14,7 +14,7 @@ import { useParams } from "react-router-dom"; import SubmissionCard from "./SubmissionCard"; import { Course } from "../../../types/course"; import { Title } from "../../../components/Header/Title"; -import { get_csrf_cookie } from "../../../utils/csrf"; +import { authenticatedFetch } from "../../../utils/authenticated-fetch"; const API_URL = import.meta.env.VITE_API_HOST; @@ -35,22 +35,12 @@ export default function ProjectView() { const [assignmentRawText, setAssignmentRawText] = useState(""); useEffect(() => { - fetch(`${API_URL}/projects/${projectId}`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }).then((response) => { + authenticatedFetch(`${API_URL}/projects/${projectId}`).then((response) => { if (response.ok) { response.json().then((data) => { const projectData = data["data"]; setProjectData(projectData); - fetch(`${API_URL}/courses/${projectData.course_id}`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }).then((response) => { + authenticatedFetch(`${API_URL}/courses/${projectData.course_id}`).then((response) => { if (response.ok) { response.json().then((data) => { setCourseData(data["data"]); @@ -61,12 +51,7 @@ export default function ProjectView() { } }); - fetch(`${API_URL}/projects/${projectId}/assignment`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }).then((response) => { + authenticatedFetch(`${API_URL}/projects/${projectId}/assignment`).then((response) => { if (response.ok) { response.text().then((data) => setAssignmentRawText(data)); } diff --git a/frontend/src/pages/project/projectView/SubmissionCard.tsx b/frontend/src/pages/project/projectView/SubmissionCard.tsx index d22b64db..d9d8a898 100644 --- a/frontend/src/pages/project/projectView/SubmissionCard.tsx +++ b/frontend/src/pages/project/projectView/SubmissionCard.tsx @@ -16,7 +16,7 @@ import axios from "axios"; import { useTranslation } from "react-i18next"; import SubmissionsGrid from "./SubmissionsGrid"; import { Submission } from "../../../types/submission"; -import { get_csrf_cookie } from "../../../utils/csrf"; +import { authenticatedFetch } from "../../../utils/authenticated-fetch"; interface SubmissionCardProps { regexRequirements?: string[]; @@ -48,12 +48,7 @@ export default function SubmissionCard({ useEffect(() => { - fetch(`${submissionUrl}?project_id=${projectId}`, { - credentials: 'include', - headers: { - "X-CSRF-TOKEN": get_csrf_cookie() - }, - }).then((response) => { + authenticatedFetch(`${submissionUrl}?project_id=${projectId}`).then((response) => { if (response.ok) { response.json().then((data) => { setPreviousSubmissions(data["data"]); diff --git a/frontend/src/utils/authenticated-fetch.ts b/frontend/src/utils/authenticated-fetch.ts new file mode 100644 index 00000000..84f1b2e0 --- /dev/null +++ b/frontend/src/utils/authenticated-fetch.ts @@ -0,0 +1,17 @@ +import { getCSRFCookie } from "./csrf"; + +/** + * A helper function to automatically add the necessary authentication options to fetch + * @returns the result of the fetch with given options and default authentication options included + */ +export function authenticatedFetch( + url: string, + init?: RequestInit +): Promise { + const update = { ...init, credentials: "include"}; + update.headers = { + ...update.headers, + "X-CSRF-TOKEN": getCSRFCookie() + } + return fetch(url, Object.assign(update)); +} diff --git a/frontend/src/utils/csrf.ts b/frontend/src/utils/csrf.ts index cce72dca..195e98f3 100644 --- a/frontend/src/utils/csrf.ts +++ b/frontend/src/utils/csrf.ts @@ -2,7 +2,7 @@ * A helper function to easily retrieve the crsf_access_token cookie * @returns the crsf_access_token cookie */ -export function get_csrf_cookie(): string { +export function getCSRFCookie(): string { const cookie = document.cookie .split("; ") .find((row) => row.startsWith("csrf_access_token=")) From e82d4fef2cdf445ed820019ac6bc2f7382fb21d3 Mon Sep 17 00:00:00 2001 From: Siebe Vlietinck Date: Thu, 25 Apr 2024 16:50:51 +0200 Subject: [PATCH 9/9] removed e --- frontend/src/pages/project/FetchProjects.tsx | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/frontend/src/pages/project/FetchProjects.tsx b/frontend/src/pages/project/FetchProjects.tsx index 64194a99..dd4ba700 100644 --- a/frontend/src/pages/project/FetchProjects.tsx +++ b/frontend/src/pages/project/FetchProjects.tsx @@ -54,7 +54,9 @@ export const fetchProjects = async () => { )[0]; // fetch the course id of the project const project_item = await ( - await authenticatedFetch(encodeURI(`${API_URL}/projects/${project_id}`)) + await authenticatedFetch( + encodeURI(`${API_URL}/projects/${project_id}`) + ) ).json(); //fetch the course @@ -119,7 +121,7 @@ export const fetchProjects = async () => { ); formattedData = formattedData.flat(); return formattedData; - } catch (e) { + } catch (_) { return []; } };