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