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/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/components/Courses/CourseDetailTeacher.tsx b/frontend/src/components/Courses/CourseDetailTeacher.tsx
index e1cf0ff3..cd729766 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 { authenticatedFetch } from "../../utils/authenticated-fetch";
interface UserUid{
uid: string
@@ -18,12 +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"
- },
body: JSON.stringify({
"admin_uid": uid
})
@@ -40,11 +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"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
"students": uids
@@ -61,9 +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',
}).then((response) => {
if(response.ok){
navigate(-1);
@@ -289,9 +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',
})
.then(response => response.json())
.then(data => {
@@ -317,11 +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'
+ 'Content-Type': 'application/json',
},
body: JSON.stringify(bodyContent)
})
@@ -329,12 +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"
+ "Content-Type": "application/json",
},
body: JSON.stringify({
"join_code": joinCode
diff --git a/frontend/src/components/Courses/CourseUtilComponents.tsx b/frontend/src/components/Courses/CourseUtilComponents.tsx
index 6a9e4804..48d6bdb2 100644
--- a/frontend/src/components/Courses/CourseUtilComponents.tsx
+++ b/frontend/src/components/Courses/CourseUtilComponents.tsx
@@ -1,17 +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 { authenticatedFetch } from "../../utils/authenticated-fetch";
/**
* @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}
+
);
}
@@ -21,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 (
@@ -41,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) => {
@@ -85,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);
};
@@ -98,11 +152,10 @@ 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' }
- )
- .then(response => response.json())
+ const projectPromises = courses.map((course) =>
+ authenticatedFetch(
+ `${apiHost}/projects?course_id=${getIdFromLink(course.course_id)}`
+ ).then((response) => response.json())
);
const projectResults = await Promise.all(projectPromises);
@@ -118,59 +171,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")}:
+
-
+
+
+
@@ -180,36 +279,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 (
-
-
+
+
);
@@ -217,4 +331,4 @@ function EmptyOrNotProjects({projects, noProjectsText}: {projects: Project[], no
>
);
}
-}
\ No newline at end of file
+}
diff --git a/frontend/src/components/Courses/CourseUtils.tsx b/frontend/src/components/Courses/CourseUtils.tsx
index 51a462a2..331b5ea5 100644
--- a/frontend/src/components/Courses/CourseUtils.tsx
+++ b/frontend/src/components/Courses/CourseUtils.tsx
@@ -1,17 +1,18 @@
-import { NavigateFunction, Params } from 'react-router-dom';
-
-export interface Course{
- course_id: string,
- name: string,
- teacher:string,
- ufora_id:string,
- url:string
+import { NavigateFunction, Params } from "react-router-dom";
+import { authenticatedFetch } from "../../utils/authenticated-fetch";
+
+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;
@@ -19,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";
}
@@ -35,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";
}
@@ -44,36 +45,37 @@ 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){
- fetch(`${apiHost}/courses`, {
- credentials: 'include', // include, *same-origin, omit
+export function callToApiToCreateCourse(
+ data: string,
+ navigate: NavigateFunction
+) {
+ authenticatedFetch(`${apiHost}/courses`, {
headers: {
- "Content-Type": "application/json"
+ "Content-Type": "application/json",
},
- 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',
+ authenticatedFetch(`${apiHost}/courses/${getIdFromLink(data.url)}/admins`, {
+ method: "POST",
headers: {
- 'Content-Type': 'application/json'
+ "Content-Type": "application/json",
},
- 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];
}
@@ -84,9 +86,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
+ );
}
/**
@@ -96,14 +102,12 @@ 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'
- });
- if(res.status !== 200){
- throw new Response("Failed to fetch data", {status: res.status});
+ const res = await authenticatedFetch(uri);
+ if (res.status !== 200) {
+ throw new Response("Failed to fetch data", { status: res.status });
}
const jsonResult = await res.json();
@@ -132,7 +136,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.");
@@ -141,6 +149,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/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/components/ProjectForm/ProjectForm.tsx b/frontend/src/components/ProjectForm/ProjectForm.tsx
index e47f5354..b9410bc5 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 { authenticatedFetch } from "../../utils/authenticated-fetch.ts";
interface Course {
course_id: string;
@@ -136,9 +137,7 @@ export default function ProjectForm() {
}
const fetchCourses = async () => {
- const response = await fetch(`${apiUrl}/courses?teacher=${user}`, {
- credentials: 'include'
- })
+ const response = await authenticatedFetch(`${apiUrl}/courses?teacher=${user}`)
const jsonData = await response.json();
if (jsonData.data) {
setCourses(jsonData.data);
@@ -202,10 +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
+ body: formData,
})
if (!response.ok) {
diff --git a/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx b/frontend/src/components/ProjectSubmissionOverview/ProjectSubmissionOverview.tsx
index f0937995..2e26bb07 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 { authenticatedFetch } from "../../utils/authenticated-fetch.ts";
const apiUrl = import.meta.env.VITE_API_HOST
/**
@@ -18,18 +19,14 @@ export default function ProjectSubmissionOverview() {
});
const fetchProject = async () => {
- const response = await fetch(`${apiUrl}/projects/${projectId}`, {
- credentials: 'include'
- })
+ 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',
- })
+ 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 0c5fb167..a617d47e 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 { authenticatedFetch } from "../../utils/authenticated-fetch";
const apiUrl = import.meta.env.VITE_API_HOST
@@ -28,9 +29,7 @@ function getRowId(row: Submission) {
}
const fetchSubmissionsFromUser = async (submission_id: string) => {
- await fetch(`${apiUrl}/submissions/${submission_id}/download`, {
- credentials: 'include',
- })
+ await authenticatedFetch(`${apiUrl}/submissions/${submission_id}/download`)
.then(res => {
return res.blob();
})
@@ -83,9 +82,7 @@ export default function ProjectSubmissionsOverviewDatagrid() {
});
const fetchLastSubmissionsByUser = async () => {
- const response = await fetch(`${apiUrl}/projects/${projectId}/latest-per-user`, {
- credentials: 'include',
- })
+ 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 81444303..dd4ba700 100644
--- a/frontend/src/pages/project/FetchProjects.tsx
+++ b/frontend/src/pages/project/FetchProjects.tsx
@@ -1,114 +1,127 @@
-import {Project, ProjectDeadline, ShortSubmission} from "./projectDeadline/ProjectDeadline.tsx";
-const API_URL = import.meta.env.VITE_APP_API_HOST
+import { authenticatedFetch } from "../../utils/authenticated-fetch.ts";
+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'
- })
- if(response.status == 200){
- const data = await response.json()
- return data.role
- }else {
- return "UNKNOWN"
+ const response = await authenticatedFetch(`${API_URL}/me`);
+ 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{
- const response = await fetch(`${API_URL}/projects`, {
- credentials: 'include'
-
- })
+ try {
+ const response = await authenticatedFetch(`${API_URL}/projects`);
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'
-
- })).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 authenticatedFetch(
+ encodeURI(`${API_URL}/submissions?project_id=${project_id}`)
+ )
+ ).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'
- })).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 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'
- })).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 authenticatedFetch(
+ encodeURI(`${API_URL}/courses/${project_item.data.course_id}`)
+ )
+ ).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
- } catch (e) {
- return []
+ })
+ );
+ formattedData = formattedData.flat();
+ return formattedData;
+ } catch (_) {
+ return [];
}
-}
+};
diff --git a/frontend/src/pages/project/projectView/ProjectView.tsx b/frontend/src/pages/project/projectView/ProjectView.tsx
index 65053f4d..80335194 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 { authenticatedFetch } from "../../../utils/authenticated-fetch";
const API_URL = import.meta.env.VITE_API_HOST;
@@ -34,16 +35,12 @@ export default function ProjectView() {
const [assignmentRawText, setAssignmentRawText] = useState("");
useEffect(() => {
- fetch(`${API_URL}/projects/${projectId}`, {
- credentials: 'include',
- }).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',
- }).then((response) => {
+ authenticatedFetch(`${API_URL}/courses/${projectData.course_id}`).then((response) => {
if (response.ok) {
response.json().then((data) => {
setCourseData(data["data"]);
@@ -54,9 +51,7 @@ export default function ProjectView() {
}
});
- fetch(`${API_URL}/projects/${projectId}/assignment`, {
- credentials: 'include',
- }).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 84016e54..d9d8a898 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 { authenticatedFetch } from "../../../utils/authenticated-fetch";
interface SubmissionCardProps {
regexRequirements?: string[];
@@ -47,9 +48,7 @@ export default function SubmissionCard({
useEffect(() => {
- fetch(`${submissionUrl}?project_id=${projectId}`, {
- credentials: 'include'
- }).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
new file mode 100644
index 00000000..195e98f3
--- /dev/null
+++ b/frontend/src/utils/csrf.ts
@@ -0,0 +1,11 @@
+/**
+ * A helper function to easily retrieve the crsf_access_token cookie
+ * @returns the crsf_access_token cookie
+ */
+export function getCSRFCookie(): string {
+ const cookie = document.cookie
+ .split("; ")
+ .find((row) => row.startsWith("csrf_access_token="))
+ ?.split("=")[1];
+ return cookie ? cookie : "";
+}
\ No newline at end of file