From 6cb8a1b98fbd05baaad5ebc7d019dd9eaf71c5f7 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 13:07:54 +0200 Subject: [PATCH 001/117] feat(backend): Endpoints voor studenten beschermd --- backend/src/middleware/auth/auth.ts | 45 +++---------------- .../src/middleware/auth/checks/auth-checks.ts | 38 ++++++++++++++++ .../auth/checks/class-auth-checks.ts | 22 +++++++++ .../auth/checks/user-auth-checks.ts | 10 +++++ backend/src/routes/auth.ts | 3 +- backend/src/routes/student-join-requests.ts | 12 +++-- backend/src/routes/students.ts | 15 ++++--- 7 files changed, 93 insertions(+), 52 deletions(-) create mode 100644 backend/src/middleware/auth/checks/auth-checks.ts create mode 100644 backend/src/middleware/auth/checks/class-auth-checks.ts create mode 100644 backend/src/middleware/auth/checks/user-auth-checks.ts diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index a91932ea..54354a55 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -1,13 +1,11 @@ -import { envVars, getEnvVar } from '../../util/envVars.js'; -import { expressjwt } from 'express-jwt'; +import {envVars, getEnvVar} from '../../util/envVars.js'; +import {expressjwt} from 'express-jwt'; import * as jwt from 'jsonwebtoken'; -import { JwtPayload } from 'jsonwebtoken'; +import {JwtPayload} from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as express from 'express'; -import { AuthenticatedRequest } from './authenticated-request.js'; -import { AuthenticationInfo } from './authentication-info.js'; -import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; -import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; +import {AuthenticatedRequest} from './authenticated-request.js'; +import {AuthenticationInfo} from './authentication-info.js'; const JWKS_CACHE = true; const JWKS_RATE_LIMIT = true; @@ -108,36 +106,3 @@ function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response } export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; - -/** - * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill - * the given access condition. - * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates - * to true. - */ -export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { - return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { - if (!req.auth) { - throw new UnauthorizedException(); - } else if (!accessCondition(req.auth)) { - throw new ForbiddenException(); - } else { - next(); - } - }; -} - -/** - * Middleware which rejects all unauthenticated users, but accepts all authenticated users. - */ -export const authenticatedOnly = authorize((_) => true); - -/** - * Middleware which rejects requests from unauthenticated users or users that aren't students. - */ -export const studentsOnly = authorize((auth) => auth.accountType === 'student'); - -/** - * Middleware which rejects requests from unauthenticated users or users that aren't teachers. - */ -export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts new file mode 100644 index 00000000..bf15e551 --- /dev/null +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -0,0 +1,38 @@ +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; +import * as express from "express"; +import {UnauthorizedException} from "../../../exceptions/unauthorized-exception"; +import {ForbiddenException} from "../../../exceptions/forbidden-exception"; + +/** + * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill + * the given access condition. + * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates + * to true. + */ +export function authorize( + accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise +) { + return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise => { + if (!req.auth) { + throw new UnauthorizedException(); + } else if (!await accessCondition(req.auth, req)) { + throw new ForbiddenException(); + } else { + next(); + } + }; +} + +/** + * Middleware which rejects all unauthenticated users, but accepts all authenticated users. + */ +export const authenticatedOnly = authorize((_) => true); +/** + * Middleware which rejects requests from unauthenticated users or users that aren't students. + */ +export const studentsOnly = authorize((auth) => auth.accountType === 'student'); +/** + * Middleware which rejects requests from unauthenticated users or users that aren't teachers. + */ +export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts new file mode 100644 index 00000000..6c92e190 --- /dev/null +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -0,0 +1,22 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; +import {getClassesByTeacher} from "../../../services/teachers"; + +/** + * To be used on a request with path parameters username and classId. + * Only allows requests whose username parameter is equal to the username of the user who is logged in and requests + * whose classId parameter references a class the logged-in user is a teacher of. + */ +export const onlyAllowStudentHimselfAndTeachersOfClass = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + if (req.params.username === auth.username) { + return true; + } else if (auth.accountType === "teacher") { + const classes: string[] = (await getClassesByTeacher(auth.username, false)) as string[]; + return req.params.classId in classes; + } else { + return false; + } + } +); diff --git a/backend/src/middleware/auth/checks/user-auth-checks.ts b/backend/src/middleware/auth/checks/user-auth-checks.ts new file mode 100644 index 00000000..10814eb8 --- /dev/null +++ b/backend/src/middleware/auth/checks/user-auth-checks.ts @@ -0,0 +1,10 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; + +/** + * Only allow the user whose username is in the path parameter "username" to access the endpoint. + */ +export const onlyAllowUserHimself = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username +); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 4a1f27d2..8c4ab450 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,6 +1,7 @@ import express from 'express'; import { getFrontendAuthConfig } from '../controllers/auth.js'; -import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; +import {authenticatedOnly, studentsOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; + const router = express.Router(); // Returns auth configuration for frontend diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts index daf79f09..66a6c75e 100644 --- a/backend/src/routes/student-join-requests.ts +++ b/backend/src/routes/student-join-requests.ts @@ -5,15 +5,19 @@ import { getStudentRequestHandler, getStudentRequestsHandler, } from '../controllers/students.js'; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import {onlyAllowStudentHimselfAndTeachersOfClass} from "../middleware/auth/checks/class-auth-checks"; + +// Under /:username/joinRequests/ const router = express.Router({ mergeParams: true }); -router.get('/', getStudentRequestsHandler); +router.get('/', onlyAllowUserHimself, getStudentRequestsHandler); -router.post('/', createStudentRequestHandler); +router.post('/', onlyAllowUserHimself, createStudentRequestHandler); -router.get('/:classId', getStudentRequestHandler); +router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler); -router.delete('/:classId', deleteClassJoinRequestHandler); +router.delete('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, deleteClassJoinRequestHandler); export default router; diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 0f5d5349..54b0d894 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -11,6 +11,7 @@ import { getStudentSubmissionsHandler, } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; const router = express.Router(); @@ -19,25 +20,25 @@ router.get('/', getAllStudentsHandler); router.post('/', createStudentHandler); -router.delete('/:username', deleteStudentHandler); +router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); // Information about a student's profile -router.get('/:username', getStudentHandler); +router.get('/:username', onlyAllowUserHimself, getStudentHandler); // The list of classes a student is in -router.get('/:username/classes', getStudentClassesHandler); +router.get('/:username/classes', onlyAllowUserHimself, getStudentClassesHandler); // The list of submissions a student has made -router.get('/:username/submissions', getStudentSubmissionsHandler); +router.get('/:username/submissions', onlyAllowUserHimself, getStudentSubmissionsHandler); // The list of assignments a student has -router.get('/:username/assignments', getStudentAssignmentsHandler); +router.get('/:username/assignments', onlyAllowUserHimself, getStudentAssignmentsHandler); // The list of groups a student is in -router.get('/:username/groups', getStudentGroupsHandler); +router.get('/:username/groups', onlyAllowUserHimself, getStudentGroupsHandler); // A list of questions a user has created -router.get('/:username/questions', getStudentQuestionsHandler); +router.get('/:username/questions', onlyAllowUserHimself, getStudentQuestionsHandler); router.use('/:username/joinRequests', joinRequestRouter); From 9339eca9cfa9fc9d9f0c87106565603b7a516878 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 14:24:57 +0200 Subject: [PATCH 002/117] feat: Mechanisme voor automatische aanmaak en update van accounts aangemaakt. --- backend/src/controllers/auth.ts | 30 ++++++++++++++++++- .../src/middleware/auth/checks/auth-checks.ts | 5 ++++ .../auth/checks/class-auth-checks.ts | 19 ++++++++++-- backend/src/routes/auth.ts | 10 ++++--- backend/src/routes/students.ts | 7 +++-- backend/src/routes/teachers.ts | 21 +++++++------ backend/src/services/students.ts | 4 +-- backend/src/services/teachers.ts | 4 +-- frontend/src/services/auth/auth-service.ts | 7 +++++ 9 files changed, 84 insertions(+), 23 deletions(-) diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index b87eaf7b..e30a412e 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,4 +1,9 @@ import { envVars, getEnvVar } from '../util/envVars.js'; +import {AuthenticatedRequest} from "../middleware/auth/authenticated-request"; +import {createStudent} from "../services/students"; +import {AuthenticationInfo} from "../middleware/auth/authentication-info"; +import {Request, Response} from "express"; +import {createTeacher} from "../services/teachers"; interface FrontendIdpConfig { authority: string; @@ -15,7 +20,7 @@ interface FrontendAuthConfig { const SCOPE = 'openid profile email'; const RESPONSE_TYPE = 'code'; -export function getFrontendAuthConfig(): FrontendAuthConfig { +function getFrontendAuthConfig(): FrontendAuthConfig { return { student: { authority: getEnvVar(envVars.IdpStudentUrl), @@ -31,3 +36,26 @@ export function getFrontendAuthConfig(): FrontendAuthConfig { }, }; } + +export function handleGetFrontendAuthConfig(_req: Request, res: Response): void { + res.json(getFrontendAuthConfig()); +} + +export async function handleHello(req: AuthenticatedRequest) { + const auth: AuthenticationInfo = req.auth!; + if (auth.accountType === "teacher") { + await createTeacher({ + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? "", + lastName: auth.lastName ?? "", + }, true); + } else { + await createStudent({ + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? "", + lastName: auth.lastName ?? "", + }, true); + } +} diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts index bf15e551..ee70c109 100644 --- a/backend/src/middleware/auth/checks/auth-checks.ts +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -36,3 +36,8 @@ export const studentsOnly = authorize((auth) => auth.accountType === 'student'); * Middleware which rejects requests from unauthenticated users or users that aren't teachers. */ export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); +/** + * Middleware which is to be used on requests no normal user should be able to execute. + * Since there is no concept of administrator accounts yet, currently, those requests will always be blocked. + */ +export const adminOnly = authorize(() => false); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 6c92e190..13adf4a8 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -1,7 +1,12 @@ import {authorize} from "./auth-checks"; import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; -import {getClassesByTeacher} from "../../../services/teachers"; +import {getClass} from "../../../services/classes"; + +async function teaches(teacherUsername: string, classId: string) { + const clazz = await getClass(classId); + return clazz != null && teacherUsername in clazz.teachers; +} /** * To be used on a request with path parameters username and classId. @@ -13,10 +18,18 @@ export const onlyAllowStudentHimselfAndTeachersOfClass = authorize( if (req.params.username === auth.username) { return true; } else if (auth.accountType === "teacher") { - const classes: string[] = (await getClassesByTeacher(auth.username, false)) as string[]; - return req.params.classId in classes; + return teaches(auth.username, req.params.classId); } else { return false; } } ); + +/** + * Only let the request pass through if its path parameter "username" is the username of the currently logged-in + * teacher and the path parameter "classId" refers to a class the teacher teaches. + */ +export const onlyAllowTeacherOfClass = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => + req.params.username === auth.username && teaches(auth.username, req.params.classId), +); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 8c4ab450..7f204076 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,13 +1,15 @@ import express from 'express'; -import { getFrontendAuthConfig } from '../controllers/auth.js'; +import {handleGetFrontendAuthConfig, handleHello} from '../controllers/auth.js'; import {authenticatedOnly, studentsOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); // Returns auth configuration for frontend -router.get('/config', (_req, res) => { - res.json(getFrontendAuthConfig()); -}); +router.get('/config', handleGetFrontendAuthConfig); + +// This endpoint is called by the client when the user has just logged in. +// It creates or updates the user entity based on the authentication data the endpoint was called with. +router.post('/hello', authenticatedOnly, handleHello); router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 54b0d894..e378e6ea 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -12,13 +12,16 @@ import { } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import {adminOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); // Root endpoint used to search objects -router.get('/', getAllStudentsHandler); +router.get('/', adminOnly, getAllStudentsHandler); -router.post('/', createStudentHandler); +// Users will be created automatically when some resource is created for them. Therefore, this endpoint +// can only be used by an administrator. +router.post('/', adminOnly, createStudentHandler); router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index a6106a80..3e463500 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -10,26 +10,29 @@ import { getTeacherStudentHandler, updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; +import {adminOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import {onlyAllowTeacherOfClass} from "../middleware/auth/checks/class-auth-checks"; const router = express.Router(); // Root endpoint used to search objects -router.get('/', getAllTeachersHandler); +router.get('/', adminOnly, getAllTeachersHandler); -router.post('/', createTeacherHandler); +router.post('/', adminOnly, createTeacherHandler); -router.get('/:username', getTeacherHandler); +router.get('/:username', onlyAllowUserHimself, getTeacherHandler); -router.delete('/:username', deleteTeacherHandler); +router.delete('/:username', onlyAllowUserHimself, deleteTeacherHandler); -router.get('/:username/classes', getTeacherClassHandler); +router.get('/:username/classes', onlyAllowUserHimself, getTeacherClassHandler); -router.get('/:username/students', getTeacherStudentHandler); +router.get('/:username/students', onlyAllowUserHimself, getTeacherStudentHandler); -router.get('/:username/questions', getTeacherQuestionHandler); +router.get('/:username/questions', onlyAllowUserHimself, getTeacherQuestionHandler); -router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); +router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler); -router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); +router.put('/:username/joinRequests/:classId/:studentUsername', onlyAllowTeacherOfClass, updateStudentJoinRequestHandler); // Invitations to other classes a teacher received router.get('/:id/invitations', (_req, res) => { diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 960edb93..6158a0f8 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -52,11 +52,11 @@ export async function getStudent(username: string): Promise { return mapToStudentDTO(user); } -export async function createStudent(userData: StudentDTO): Promise { +export async function createStudent(userData: StudentDTO, allowUpdate: boolean = false): Promise { const studentRepository = getStudentRepository(); const newStudent = mapToStudent(userData); - await studentRepository.save(newStudent, { preventOverwrite: true }); + await studentRepository.save(newStudent, { preventOverwrite: !allowUpdate }); return userData; } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 1b7643fb..501f03f0 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -56,11 +56,11 @@ export async function getTeacher(username: string): Promise { return mapToTeacherDTO(user); } -export async function createTeacher(userData: TeacherDTO): Promise { +export async function createTeacher(userData: TeacherDTO, update?: boolean): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); const newTeacher = mapToTeacher(userData); - await teacherRepository.save(newTeacher, { preventOverwrite: true }); + await teacherRepository.save(newTeacher, { preventOverwrite: !update }); return mapToTeacherDTO(newTeacher); } diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 977e1dbf..0cb62803 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -29,6 +29,10 @@ const authState = reactive({ activeRole: authStorage.getActiveRole() || null, }); +async function sendHello(): Promise { + return apiClient.post("/auth/hello"); +} + /** * Load the information about who is currently logged in from the IDP. */ @@ -41,6 +45,8 @@ async function loadUser(): Promise { authState.user = user; authState.accessToken = user?.access_token || null; authState.activeRole = activeRole || null; + + await sendHello(); return user; } @@ -72,6 +78,7 @@ async function handleLoginCallback(): Promise { throw new Error("Login callback received, but the user is not logging in!"); } authState.user = (await (await getUserManagers())[activeRole].signinCallback()) || null; + await sendHello(); } /** From 22523262344eac9be7ee6a598ccc556f5b6befc6 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 15:45:20 +0200 Subject: [PATCH 003/117] feat(backend): Endpoints van klassen en leerkrachten beschermd. --- backend/src/controllers/auth.ts | 2 +- .../auth/checks/class-auth-checks.ts | 23 +++++++++++++++++++ backend/src/routes/classes.ts | 12 ++++++---- 3 files changed, 31 insertions(+), 6 deletions(-) diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index e30a412e..4df138c1 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -41,7 +41,7 @@ export function handleGetFrontendAuthConfig(_req: Request, res: Response): void res.json(getFrontendAuthConfig()); } -export async function handleHello(req: AuthenticatedRequest) { +export async function handleHello(req: AuthenticatedRequest): Promise { const auth: AuthenticationInfo = req.auth!; if (auth.accountType === "teacher") { await createTeacher({ diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 13adf4a8..f51cf740 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -33,3 +33,26 @@ export const onlyAllowTeacherOfClass = authorize( async (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username && teaches(auth.username, req.params.classId), ); + +/** + * Only let the request pass through if the class id in it refers to a class the current user is in (as a student + * or teacher) + */ +function createOnlyAllowIfInClass(onlyTeacher: boolean) { + return authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const classId = req.params.classId ?? req.params.classid ?? req.params.id; + const clazz = await getClass(classId); + if (clazz == null) { + return false; + } else if (onlyTeacher || auth.accountType === "teacher") { + return auth.username in clazz.teachers; + } else { + return auth.username in clazz.students; + } + } + ); +} + +export const onlyAllowIfInClass = createOnlyAllowIfInClass(false); +export const onlyAllowIfTeacherInClass = createOnlyAllowIfInClass(true); diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index e0972988..f2dd7686 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -7,20 +7,22 @@ import { getTeacherInvitationsHandler, } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; +import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowIfInClass, onlyAllowIfTeacherInClass} from "../middleware/auth/checks/class-auth-checks"; const router = express.Router(); // Root endpoint used to search objects -router.get('/', getAllClassesHandler); +router.get('/', adminOnly, getAllClassesHandler); -router.post('/', createClassHandler); +router.post('/', teachersOnly, createClassHandler); // Information about an class with id 'id' -router.get('/:id', getClassHandler); +router.get('/:id', onlyAllowIfInClass, getClassHandler); -router.get('/:id/teacher-invitations', getTeacherInvitationsHandler); +router.get('/:id/teacher-invitations', onlyAllowIfTeacherInClass, getTeacherInvitationsHandler); -router.get('/:id/students', getClassStudentsHandler); +router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler); router.use('/:classid/assignments', assignmentRouter); From a1ce8a209c15a378674c10ccdca5b1125774ae8c Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 15:46:39 +0200 Subject: [PATCH 004/117] feat(backend): Endpoints van thema's beschermd --- backend/src/routes/themes.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/themes.ts b/backend/src/routes/themes.ts index b135d44f..089c5d46 100644 --- a/backend/src/routes/themes.ts +++ b/backend/src/routes/themes.ts @@ -1,14 +1,15 @@ import express from 'express'; import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js'; +import {authenticatedOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); // Query: language // Route to fetch list of {key, title, description, image} themes in their respective language -router.get('/', getThemesHandler); +router.get('/', authenticatedOnly, getThemesHandler); // Arg: theme (key) // Route to fetch list of hruids based on theme -router.get('/:theme', getHruidsByThemeHandler); +router.get('/:theme', authenticatedOnly, getHruidsByThemeHandler); export default router; From bc2cd145ab7d96e9b07d7ecd2339095231269e50 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 8 Apr 2025 16:58:14 +0200 Subject: [PATCH 005/117] feat(backend): Endpoints van assignments en groepen beschermd. --- backend/src/controllers/assignments.ts | 2 +- .../auth/authenticated-request.d.ts | 9 +++- .../auth/checks/assignment-auth-checks.ts | 26 ++++++++++ .../src/middleware/auth/checks/auth-checks.ts | 9 ++-- .../auth/checks/class-auth-checks.ts | 51 +++++++++++-------- .../auth/checks/group-auth-checker.ts | 24 +++++++++ backend/src/routes/assignments.ts | 13 +++-- backend/src/routes/classes.ts | 4 +- backend/src/routes/groups.ts | 7 +-- backend/src/routes/students.ts | 2 +- backend/src/services/students.ts | 2 +- 11 files changed, 111 insertions(+), 38 deletions(-) create mode 100644 backend/src/middleware/auth/checks/assignment-auth-checks.ts create mode 100644 backend/src/middleware/auth/checks/group-auth-checker.ts diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 1520fc10..d92def19 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -3,7 +3,7 @@ import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmi import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; // Typescript is annoying with parameter forwarding from class.ts -interface AssignmentParams { +export interface AssignmentParams { classid: string; id: string; } diff --git a/backend/src/middleware/auth/authenticated-request.d.ts b/backend/src/middleware/auth/authenticated-request.d.ts index 9737fa7e..c0049dc7 100644 --- a/backend/src/middleware/auth/authenticated-request.d.ts +++ b/backend/src/middleware/auth/authenticated-request.d.ts @@ -1,8 +1,15 @@ import { Request } from 'express'; import { JwtPayload } from 'jsonwebtoken'; import { AuthenticationInfo } from './authentication-info.js'; +import * as core from "express-serve-static-core"; -export interface AuthenticatedRequest extends Request { +export interface AuthenticatedRequest< + P = core.ParamsDictionary, + ResBody = unknown, + ReqBody = unknown, + ReqQuery = core.Query, + Locals extends Record = Record, +> extends Request { // Properties are optional since the user is not necessarily authenticated. jwtPayload?: JwtPayload; auth?: AuthenticationInfo; diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts new file mode 100644 index 00000000..a9bbe87f --- /dev/null +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -0,0 +1,26 @@ +import {authorize} from "./auth-checks"; +import {getAssignment} from "../../../services/assignments"; +import {getClass} from "../../../services/classes"; +import {getAllGroups} from "../../../services/groups"; + +/** + * Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment). + * Only allows requests from users who are + * - either teachers of the class the assignment was posted in, + * - or students in a group of the assignment. + */ +export const onlyAllowIfHasAccessToAssignment = authorize( + async (auth, req) => { + const { classid: classId, id: assignmentId } = req.params as { classid: string, id: number }; + const assignment = await getAssignment(classId, assignmentId); + if (assignment === null) { + return false; + } else if (auth.accountType === "teacher") { + const clazz = await getClass(assignment.class); + return auth.username in clazz!.teachers; + } else { + const groups = await getAllGroups(classId, assignmentId, false); + return groups.some(group => auth.username in (group.members as string[])); + } + } +); diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts index ee70c109..d14e8dee 100644 --- a/backend/src/middleware/auth/checks/auth-checks.ts +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -3,6 +3,7 @@ import {AuthenticatedRequest} from "../authenticated-request"; import * as express from "express"; import {UnauthorizedException} from "../../../exceptions/unauthorized-exception"; import {ForbiddenException} from "../../../exceptions/forbidden-exception"; +import {RequestHandler} from "express"; /** * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill @@ -10,10 +11,10 @@ import {ForbiddenException} from "../../../exceptions/forbidden-exception"; * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * to true. */ -export function authorize( - accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise -) { - return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise => { +export function authorize>( + accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise +): RequestHandler { + return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise => { if (!req.auth) { throw new UnauthorizedException(); } else if (!await accessCondition(req.auth, req)) { diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index f51cf740..c89a263f 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -3,9 +3,9 @@ import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; import {getClass} from "../../../services/classes"; -async function teaches(teacherUsername: string, classId: string) { +async function teaches(teacherUsername: string, classId: string): Promise { const clazz = await getClass(classId); - return clazz != null && teacherUsername in clazz.teachers; + return clazz !== null && teacherUsername in clazz.teachers; } /** @@ -19,9 +19,9 @@ export const onlyAllowStudentHimselfAndTeachersOfClass = authorize( return true; } else if (auth.accountType === "teacher") { return teaches(auth.username, req.params.classId); - } else { - return false; } + return false; + } ); @@ -38,21 +38,32 @@ export const onlyAllowTeacherOfClass = authorize( * Only let the request pass through if the class id in it refers to a class the current user is in (as a student * or teacher) */ -function createOnlyAllowIfInClass(onlyTeacher: boolean) { - return authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const classId = req.params.classId ?? req.params.classid ?? req.params.id; - const clazz = await getClass(classId); - if (clazz == null) { - return false; - } else if (onlyTeacher || auth.accountType === "teacher") { - return auth.username in clazz.teachers; - } else { - return auth.username in clazz.students; - } +export const onlyAllowIfInClass = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const classId = req.params.classId ?? req.params.classid ?? req.params.id; + const clazz = await getClass(classId); + if (clazz === null) { + return false; + } else if (auth.accountType === "teacher") { + return auth.username in clazz.teachers; } - ); -} + return auth.username in clazz.students; + } +); -export const onlyAllowIfInClass = createOnlyAllowIfInClass(false); -export const onlyAllowIfTeacherInClass = createOnlyAllowIfInClass(true); +/** + * Only allows the request to pass if the 'class' property in its body is a class the current user is a member of. + */ +export const onlyAllowOwnClassInBody = authorize( + async (auth, req) => { + const classId = (req.body as {class: string})?.class; + const clazz = await getClass(classId); + + if (clazz === null) { + return false; + } else if (auth.accountType === "teacher") { + return auth.username in clazz.teachers; + } + return auth.username in clazz.students; + } +); diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts new file mode 100644 index 00000000..f62ab3b9 --- /dev/null +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -0,0 +1,24 @@ +import {authorize} from "./auth-checks"; +import {getClass} from "../../../services/classes"; +import {getGroup} from "../../../services/groups"; + +/** + * Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'. + * Only allows requests from users who are + * - either teachers of the class the assignment for the group was posted in, + * - or students in the group + */ +export const onlyAllowIfHasAccessToGroup = authorize( + async (auth, req) => { + const { classid: classId, assignmentid: assignmentId, groupid: groupId } = + req.params as { classid: string, assignmentid: number, groupid: number }; + + if (auth.accountType === "teacher") { + const clazz = await getClass(classId); + return auth.username in clazz!.teachers; + } else { // user is student + const group = await getGroup(classId, assignmentId, groupId, false); + return group === null ? false : auth.username in (group.members as string[]); + } + } +); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index 3652dcc6..4db12769 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -6,20 +6,23 @@ import { getAssignmentsSubmissionsHandler, } from '../controllers/assignments.js'; import groupRouter from './groups.js'; +import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowOwnClassInBody} from "../middleware/auth/checks/class-auth-checks"; +import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks"; const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', getAllAssignmentsHandler); +router.get('/', adminOnly, getAllAssignmentsHandler); -router.post('/', createAssignmentHandler); +router.post('/', teachersOnly, onlyAllowOwnClassInBody, createAssignmentHandler); // Information about an assignment with id 'id' -router.get('/:id', getAssignmentHandler); +router.get('/:id', onlyAllowIfHasAccessToAssignment, getAssignmentHandler); -router.get('/:id/submissions', getAssignmentsSubmissionsHandler); +router.get('/:id/submissions', onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler); -router.get('/:id/questions', (_req, res) => { +router.get('/:id/questions', onlyAllowIfHasAccessToAssignment, (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index f2dd7686..36ba82e1 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -8,7 +8,7 @@ import { } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowIfInClass, onlyAllowIfTeacherInClass} from "../middleware/auth/checks/class-auth-checks"; +import {onlyAllowIfInClass} from "../middleware/auth/checks/class-auth-checks"; const router = express.Router(); @@ -20,7 +20,7 @@ router.post('/', teachersOnly, createClassHandler); // Information about an class with id 'id' router.get('/:id', onlyAllowIfInClass, getClassHandler); -router.get('/:id/teacher-invitations', onlyAllowIfTeacherInClass, getTeacherInvitationsHandler); +router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler); router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 1486edce..e8e83fb2 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -1,5 +1,6 @@ import express from 'express'; import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; +import {onlyAllowIfHasAccessToGroup} from "../middleware/auth/checks/group-auth-checker"; const router = express.Router({ mergeParams: true }); @@ -9,12 +10,12 @@ router.get('/', getAllGroupsHandler); router.post('/', createGroupHandler); // Information about a group (members, ... [TODO DOC]) -router.get('/:groupid', getGroupHandler); +router.get('/:groupid', onlyAllowIfHasAccessToGroup, getGroupHandler); -router.get('/:groupid/submissions', getGroupSubmissionsHandler); +router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler); // The list of questions a group has made -router.get('/:id/questions', (_req, res) => { +router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index e378e6ea..04d260b4 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -20,7 +20,7 @@ const router = express.Router(); router.get('/', adminOnly, getAllStudentsHandler); // Users will be created automatically when some resource is created for them. Therefore, this endpoint -// can only be used by an administrator. +// Can only be used by an administrator. router.post('/', adminOnly, createStudentHandler); router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 6158a0f8..ed951a5a 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -52,7 +52,7 @@ export async function getStudent(username: string): Promise { return mapToStudentDTO(user); } -export async function createStudent(userData: StudentDTO, allowUpdate: boolean = false): Promise { +export async function createStudent(userData: StudentDTO, allowUpdate = false): Promise { const studentRepository = getStudentRepository(); const newStudent = mapToStudent(userData); From fb3c37ce5a2e84136c146bcd9a861695eb332f5f Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Wed, 9 Apr 2025 19:51:15 +0200 Subject: [PATCH 006/117] fix(backend): Merge-conflicten opgelost. --- backend/src/controllers/questions.ts | 73 ++++++++++--------- backend/src/interfaces/question.ts | 2 +- .../checks/learning-content-auth-checks.ts | 23 ++++++ backend/src/routes/groups.ts | 6 +- backend/src/routes/learning-objects.ts | 9 ++- backend/src/routes/learning-paths.ts | 3 +- backend/src/services/questions.ts | 11 +-- common/src/interfaces/question.ts | 1 + 8 files changed, 79 insertions(+), 49 deletions(-) create mode 100644 backend/src/middleware/auth/checks/learning-content-auth-checks.ts diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 282c90b8..cc677dba 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -7,11 +7,12 @@ import { getQuestion, getQuestionsAboutLearningObjectInAssignment, updateQuestion, } from '../services/questions.js'; -import { FALLBACK_LANG, FALLBACK_SEQ_NUM } from '../config.js'; +import {FALLBACK_LANG, FALLBACK_SEQ_NUM, FALLBACK_VERSION_NUM} from '../config.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { QuestionData, QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { Language } from '@dwengo-1/common/util/language'; import {requireFields} from "./error-helper"; +import {BadRequestException} from "../exceptions/bad-request-exception"; export function getLearningObjectId(hruid: string, version: string, lang: string): LearningObjectIdentifier { return { @@ -28,7 +29,7 @@ export function getQuestionId(learningObjectIdentifier: LearningObjectIdentifier }; } -function getQuestionId(req: Request, res: Response): QuestionId | null { +function getQuestionIdFromRequest(req: Request): QuestionId | null { const seq = req.params.seq; const hruid = req.params.hruid; const version = req.params.version; @@ -39,10 +40,7 @@ function getQuestionId(req: Request, res: Response): QuestionId | null { return null; } - return { - learningObjectIdentifier, - sequenceNumber: seq ? Number(seq) : FALLBACK_SEQ_NUM, - }; + return getQuestionId(learningObjectIdentifier, seq); } export async function getAllQuestionsHandler(req: Request, res: Response): Promise { @@ -52,16 +50,22 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi const full = req.query.full === 'true'; requireFields({ hruid }); + const assignmentId = parseInt(req.query.assignmentId as string); + + if (isNaN(assignmentId)) { + throw new BadRequestException("The assignment ID must be a number."); + } + const learningObjectId = getLearningObjectId(hruid, version, language); let questions: QuestionDTO[] | QuestionId[]; if (req.query.classId && req.query.assignmentId) { questions = await getQuestionsAboutLearningObjectInAssignment( learningObjectId, - req.query.classId, - req.query.assignmentId, + req.query.classId as string, + parseInt(req.query.assignmentId as string), full ?? false, - req.query.forStudent + req.query.forStudent as string | undefined ); } else { questions = await getAllQuestions(learningObjectId, full ?? false); @@ -70,40 +74,37 @@ export async function getAllQuestionsHandler(req: Request, res: Response): Promi res.json({ questions }); } - export async function getQuestionHandler(req: Request, res: Response): Promise { - const hruid = req.params.hruid; - const version = req.params.version; - const language = req.query.lang as string; - const seq = req.params.seq; - requireFields({ hruid }); +export async function getQuestionHandler(req: Request, res: Response): Promise { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }); - const learningObjectId = getLearningObjectId(hruid, version, language); - const questionId = getQuestionId(learningObjectId, seq); + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); - const question = await getQuestion(questionId); + const question = await getQuestion(questionId); - res.json({ question }); + res.json({ question }); +} + +export async function getQuestionAnswersHandler(req: Request, res: Response): Promise { + const questionId = getQuestionIdFromRequest(req); + const full = req.query.full; + + if (!questionId) { + return; } - export async function getQuestionAnswersHandler( - req: Request, - res: Response - ): Promise { - const questionId = getQuestionId(req, res); - const full = req.query.full; + const answers = await getAnswersByQuestion(questionId, full === "true"); - if (!questionId) { - return; - } - - const answers = await getAnswersByQuestion(questionId, full); - - if (!answers) { - res.status(404).json({ error: `Questions not found` }); - } else { - res.json({ answers: answers }); - } + if (!answers) { + res.status(404).json({ error: `Questions not found` }); + } else { + res.json({ answers: answers }); } +} export async function createQuestionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; diff --git a/backend/src/interfaces/question.ts b/backend/src/interfaces/question.ts index b4e58db7..ac334506 100644 --- a/backend/src/interfaces/question.ts +++ b/backend/src/interfaces/question.ts @@ -1,9 +1,9 @@ import { Question } from '../entities/questions/question.entity.js'; import { mapToStudentDTO } from './student.js'; import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; -import { LearningObjectIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import { mapToGroupDTOId } from './group'; import { LearningObjectIdentifierDTO } from '@dwengo-1/common/interfaces/learning-content'; +import {LearningObjectIdentifier} from "../entities/content/learning-object-identifier"; function getLearningObjectIdentifier(question: Question): LearningObjectIdentifierDTO { return { diff --git a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts new file mode 100644 index 00000000..57a3021d --- /dev/null +++ b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts @@ -0,0 +1,23 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; +import {getGroup} from "../../../services/groups"; + +/** + * Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId') + * are + * - either not set + * - or set to a group the user is in, + * - or set to anything if the user is a teacher. + */ +export const onlyAllowPersonalizationForOwnGroup = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const {forGroup, assignmentNo, classId} = req.params; + if (forGroup && assignmentNo && classId) { + const group = getGroup(forGroup, parseInt(assignmentNo), classId, false); + + } else { + return true; + } + } +); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index e8e83fb2..93500eb3 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -1,13 +1,15 @@ import express from 'express'; import { createGroupHandler, getAllGroupsHandler, getGroupHandler, getGroupSubmissionsHandler } from '../controllers/groups.js'; import {onlyAllowIfHasAccessToGroup} from "../middleware/auth/checks/group-auth-checker"; +import {teachersOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks"; const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', getAllGroupsHandler); +router.get('/', onlyAllowIfHasAccessToAssignment, getAllGroupsHandler); -router.post('/', createGroupHandler); +router.post('/', teachersOnly, onlyAllowIfHasAccessToAssignment, createGroupHandler); // Information about a group (members, ... [TODO DOC]) router.get('/:groupid', onlyAllowIfHasAccessToGroup, getGroupHandler); diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index 7532765b..fb65d9cd 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -3,6 +3,7 @@ import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObj import submissionRoutes from './submissions.js'; import questionRoutes from './questions.js'; +import {authenticatedOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); @@ -16,13 +17,13 @@ const router = express.Router(); // Route 2: list of object data // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie -router.get('/', getAllLearningObjects); +router.get('/', authenticatedOnly, getAllLearningObjects); // Parameter: hruid of learning object // Query: language // Route to fetch data of one learning object based on its hruid // Example: http://localhost:3000/learningObject/un_ai7 -router.get('/:hruid', getLearningObject); +router.get('/:hruid', authenticatedOnly, getLearningObject); router.use('/:hruid/submissions', submissionRoutes); @@ -32,12 +33,12 @@ router.use('/:hruid/:version/questions', questionRoutes); // Query: language, version (optional) // Route to fetch the HTML rendering of one learning object based on its hruid. // Example: http://localhost:3000/learningObject/un_ai7/html -router.get('/:hruid/html', getLearningObjectHTML); +router.get('/:hruid/html', authenticatedOnly, getLearningObjectHTML); // Parameter: hruid of learning object, name of attachment. // Query: language, version (optional). // Route to get the raw data of the attachment for one learning object based on its hruid. // Example: http://localhost:3000/learningObject/u_test/attachment/testimage.png -router.get('/:hruid/html/:attachmentName', getAttachment); +router.get('/:hruid/html/:attachmentName', authenticatedOnly, getAttachment); export default router; diff --git a/backend/src/routes/learning-paths.ts b/backend/src/routes/learning-paths.ts index efe17312..ad079551 100644 --- a/backend/src/routes/learning-paths.ts +++ b/backend/src/routes/learning-paths.ts @@ -1,5 +1,6 @@ import express from 'express'; import { getLearningPaths } from '../controllers/learning-paths.js'; +import {authenticatedOnly} from "../middleware/auth/checks/auth-checks"; const router = express.Router(); @@ -22,6 +23,6 @@ const router = express.Router(); // Route to fetch learning paths based on a theme // Example: http://localhost:3000/learningPath?theme=kiks -router.get('/', getLearningPaths); +router.get('/', authenticatedOnly, getLearningPaths); export default router; diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 45bfa750..9c719472 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -11,11 +11,13 @@ import { mapToAnswerDTO, mapToAnswerDTOId } from '../interfaces/answer.js'; import { QuestionRepository } from '../data/questions/question-repository.js'; import { LearningObjectIdentifier } from '../entities/content/learning-object-identifier.js'; import { mapToStudent } from '../interfaces/student.js'; -import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; +import {QuestionData, QuestionDTO, QuestionId} from '@dwengo-1/common/interfaces/question'; import { AnswerDTO, AnswerId } from '@dwengo-1/common/interfaces/answer'; import {fetchStudent} from "./students"; import {mapToAssignment} from "../interfaces/assignment"; import { NotFoundException } from '../exceptions/not-found-exception.js'; +import {AssignmentDTO} from "@dwengo-1/common/interfaces/assignment"; +import {FALLBACK_VERSION_NUM} from "../config"; export async function getQuestionsAboutLearningObjectInAssignment( loId: LearningObjectIdentifier, @@ -90,10 +92,9 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat const author = await fetchStudent(questionData.author!); const content = questionData.content; - const clazz = await getClassRepository().findById((questionDTO.inGroup.assignment as AssignmentDTO).class); - let questionDTO; - const assignment = mapToAssignment(questionDTO.inGroup.assignment as AssignmentDTO, clazz!); - const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionDTO.inGroup.groupNumber); + const clazz = await getClassRepository().findById((questionData.inGroup.assignment as AssignmentDTO).class); + const assignment = mapToAssignment(questionData.inGroup.assignment as AssignmentDTO, clazz!); + const inGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber))!; const question = await questionRepository.createQuestion({ loId, diff --git a/common/src/interfaces/question.ts b/common/src/interfaces/question.ts index 582e12dd..2d681fc0 100644 --- a/common/src/interfaces/question.ts +++ b/common/src/interfaces/question.ts @@ -13,6 +13,7 @@ export interface QuestionDTO { export interface QuestionData { author?: string; + inGroup: GroupDTO; content: string; } From f671341bad962c3f59a3cbaae2f54aa89bfb9af3 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Fri, 18 Apr 2025 22:33:22 +0200 Subject: [PATCH 007/117] feat: teacher invitation middelware + extra error catchings --- .../src/controllers/teacher-invitations.ts | 5 ++++ .../auth/checks/teacher-invitation-checks.ts | 23 +++++++++++++++++++ backend/src/routes/teacher-invitations.ts | 16 +++++++++---- backend/src/services/teacher-invitations.ts | 4 ++++ 4 files changed, 43 insertions(+), 5 deletions(-) create mode 100644 backend/src/middleware/auth/checks/teacher-invitation-checks.ts diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 4956f3e2..c69415ec 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -2,6 +2,7 @@ import { Request, Response } from 'express'; import { requireFields } from './error-helper'; import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; +import {ConflictException} from "../exceptions/conflict-exception"; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; @@ -30,6 +31,10 @@ export async function createInvitationHandler(req: Request, res: Response): Prom const classId = req.body.class; requireFields({ sender, receiver, classId }); + if (sender === receiver){ + throw new ConflictException("Cannot send an invitation to yourself"); + } + const data = req.body as TeacherInvitationData; const invitation = await createInvitation(data); diff --git a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts new file mode 100644 index 00000000..6ebc8512 --- /dev/null +++ b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts @@ -0,0 +1,23 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; + +export const onlyAllowSenderOrReceiver = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => + req.params.sender === auth.username || req.params.receiver === auth.username +); + +export const onlyAllowSender = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => + req.params.sender === auth.username +); + +export const onlyAllowSenderBody = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => + req.body.sender === auth.username +); + +export const onlyAllowReceiverBody = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => + req.body.receiver === auth.username +); diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 772b1351..fe0b924f 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -6,17 +6,23 @@ import { getInvitationHandler, updateInvitationHandler, } from '../controllers/teacher-invitations'; +import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import { + onlyAllowReceiverBody, onlyAllowSender, + onlyAllowSenderBody, + onlyAllowSenderOrReceiver +} from "../middleware/auth/checks/teacher-invitation-checks"; const router = express.Router({ mergeParams: true }); -router.get('/:username', getAllInvitationsHandler); +router.get('/:username', onlyAllowUserHimself, getAllInvitationsHandler); -router.get('/:sender/:receiver/:classId', getInvitationHandler); +router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver ,getInvitationHandler); -router.post('/', createInvitationHandler); +router.post('/', onlyAllowSenderBody, createInvitationHandler); -router.put('/', updateInvitationHandler); +router.put('/', onlyAllowReceiverBody, updateInvitationHandler); -router.delete('/:sender/:receiver/:classId', deleteInvitationHandler); +router.delete('/:sender/:receiver/:classId', onlyAllowSender, deleteInvitationHandler); export default router; diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 07f61bae..c50e00b1 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -32,6 +32,10 @@ export async function createInvitation(data: TeacherInvitationData): Promise Date: Fri, 18 Apr 2025 23:28:55 +0200 Subject: [PATCH 008/117] feat: question + answer checks --- .../middleware/auth/checks/question-checks.ts | 48 +++++++++++++++++++ backend/src/routes/answers.ts | 13 +++-- backend/src/routes/questions.ts | 15 ++++-- backend/src/services/answers.ts | 2 +- 4 files changed, 67 insertions(+), 11 deletions(-) create mode 100644 backend/src/middleware/auth/checks/question-checks.ts diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts new file mode 100644 index 00000000..63b47e7b --- /dev/null +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -0,0 +1,48 @@ +import {authorize} from "./auth-checks"; +import {AuthenticationInfo} from "../authentication-info"; +import {AuthenticatedRequest} from "../authenticated-request"; +import {requireFields} from "../../../controllers/error-helper"; +import {getLearningObjectId, getQuestionId} from "../../../controllers/questions"; +import {fetchQuestion} from "../../../services/questions"; +import {FALLBACK_SEQ_NUM} from "../../../config"; +import {fetchAnswer} from "../../../services/answers"; + +export const onlyAllowAuthor = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.body.author === auth.username +); + +export const onlyAllowAuthorRequest = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }); + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const question = await fetchQuestion(questionId); + + return question.author.username == auth.username; + } +); + +export const onlyAllowAuthorRequestAnswer = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + const seqAnswer = req.params.seqAnswer; + requireFields({ hruid }); + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; + const answer = await fetchAnswer(questionId, sequenceNumber); + + return answer.author.username == auth.username; + } +); diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index b74f76a0..0f11c173 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,16 +1,19 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; +import {adminOnly, authenticatedOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowAuthor, onlyAllowAuthorRequestAnswer} from "../middleware/auth/checks/question-checks"; + const router = express.Router({ mergeParams: true }); -router.get('/', getAllAnswersHandler); +router.get('/', adminOnly, getAllAnswersHandler); -router.post('/', createAnswerHandler); +router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler); -router.get('/:seqAnswer', getAnswerHandler); +router.get('/:seqAnswer', authenticatedOnly, getAnswerHandler); -router.delete('/:seqAnswer', deleteAnswerHandler); +router.delete('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, deleteAnswerHandler); -router.put('/:seqAnswer', updateAnswerHandler); +router.put('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, updateAnswerHandler); export default router; diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 5135c197..287a242b 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,20 +1,25 @@ import express from 'express'; import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; +import {adminOnly, authenticatedOnly, studentsOnly} from "../middleware/auth/checks/auth-checks"; +import {updateAnswerHandler} from "../controllers/answers"; +import {onlyAllowAuthor, onlyAllowAuthorRequest} from "../middleware/auth/checks/question-checks"; const router = express.Router({ mergeParams: true }); // Query language // Root endpoint used to search objects -router.get('/', getAllQuestionsHandler); +router.get('/', adminOnly, getAllQuestionsHandler); -router.post('/', createQuestionHandler); - -router.delete('/:seq', deleteQuestionHandler); +router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); // Information about a question with id -router.get('/:seq', getQuestionHandler); +router.get('/:seq', authenticatedOnly, getQuestionHandler); // TODO every body in group + teachers? + +router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler); + +router.put("/:seq", studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler); router.use('/:seq/answers', answerRoutes); diff --git a/backend/src/services/answers.ts b/backend/src/services/answers.ts index ab603883..7ec5773a 100644 --- a/backend/src/services/answers.ts +++ b/backend/src/services/answers.ts @@ -34,7 +34,7 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat return mapToAnswerDTO(answer); } -async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise { +export async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise { const answerRepository = getAnswerRepository(); const question = await fetchQuestion(questionId); const answer = await answerRepository.findAnswer(question, sequenceNumber); From 566bb5a5fb4003bc6ee2363485614339421b414d Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sat, 19 Apr 2025 10:58:49 +0200 Subject: [PATCH 009/117] fix: add question in group check + extra create question errors service --- .../middleware/auth/checks/question-checks.ts | 24 +++++++++++++++++++ backend/src/routes/answers.ts | 8 +++++-- backend/src/routes/questions.ts | 8 +++++-- backend/src/services/questions.ts | 13 ++++++++-- 4 files changed, 47 insertions(+), 6 deletions(-) diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts index 63b47e7b..bfe76061 100644 --- a/backend/src/middleware/auth/checks/question-checks.ts +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -6,6 +6,7 @@ import {getLearningObjectId, getQuestionId} from "../../../controllers/questions import {fetchQuestion} from "../../../services/questions"; import {FALLBACK_SEQ_NUM} from "../../../config"; import {fetchAnswer} from "../../../services/answers"; +import {mapToUsername} from "../../../interfaces/user"; export const onlyAllowAuthor = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.body.author === auth.username @@ -46,3 +47,26 @@ export const onlyAllowAuthorRequestAnswer = authorize( return answer.author.username == auth.username; } ); + +export const onlyAllowIfHasAccessToQuestion = authorize( + async (auth, req) => { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }); + + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); + + const question = await fetchQuestion(questionId); + const group = question.inGroup; + + if (auth.accountType === "teacher") { + const cls = group.assignment.within; // TODO check if contains full objects + return cls.teachers.map(mapToUsername).includes(auth.username); + } else { // user is student + return group.members.map(mapToUsername).includes(auth.username); + } + } +); diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index 0f11c173..2571c56d 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,7 +1,11 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; import {adminOnly, authenticatedOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowAuthor, onlyAllowAuthorRequestAnswer} from "../middleware/auth/checks/question-checks"; +import { + onlyAllowAuthor, + onlyAllowAuthorRequestAnswer, + onlyAllowIfHasAccessToQuestion +} from "../middleware/auth/checks/question-checks"; const router = express.Router({ mergeParams: true }); @@ -10,7 +14,7 @@ router.get('/', adminOnly, getAllAnswersHandler); router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler); -router.get('/:seqAnswer', authenticatedOnly, getAnswerHandler); +router.get('/:seqAnswer', onlyAllowIfHasAccessToQuestion, getAnswerHandler); router.delete('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, deleteAnswerHandler); diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 287a242b..1363ae1a 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -3,7 +3,11 @@ import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, g import answerRoutes from './answers.js'; import {adminOnly, authenticatedOnly, studentsOnly} from "../middleware/auth/checks/auth-checks"; import {updateAnswerHandler} from "../controllers/answers"; -import {onlyAllowAuthor, onlyAllowAuthorRequest} from "../middleware/auth/checks/question-checks"; +import { + onlyAllowAuthor, + onlyAllowAuthorRequest, + onlyAllowIfHasAccessToQuestion +} from "../middleware/auth/checks/question-checks"; const router = express.Router({ mergeParams: true }); @@ -15,7 +19,7 @@ router.get('/', adminOnly, getAllQuestionsHandler); router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); // Information about a question with id -router.get('/:seq', authenticatedOnly, getQuestionHandler); // TODO every body in group + teachers? +router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler); router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 49bf9e92..38f12689 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -12,6 +12,7 @@ import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { fetchStudent } from './students.js'; import { NotFoundException } from '../exceptions/not-found-exception'; import { FALLBACK_VERSION_NUM } from '../config.js'; +import {ConflictException} from "../exceptions/conflict-exception"; export async function getQuestionsAboutLearningObjectInAssignment( loId: LearningObjectIdentifier, @@ -88,12 +89,20 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat const clazz = await getClassRepository().findById((questionData.inGroup.assignment as AssignmentDTO).within); const assignment = mapToAssignment(questionData.inGroup.assignment as AssignmentDTO, clazz!); - const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber); + const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber); + + if (!group){ + throw new NotFoundException("Group with id and assignment not found"); + } + + if (! group.members.contains(author)) { + throw new ConflictException("Author is not part of this group"); + } const question = await questionRepository.createQuestion({ loId, author, - inGroup: inGroup!, + inGroup: group!, content, }); From cb4f6a512df065aa5aaa972c85b5383017775961 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Sat, 19 Apr 2025 11:01:26 +0200 Subject: [PATCH 010/117] fix: includes check + gebruik fetches service laag --- backend/src/interfaces/user.ts | 4 +++ .../auth/checks/assignment-auth-checks.ts | 17 +++++------ .../auth/checks/class-auth-checks.ts | 29 +++++++++---------- .../auth/checks/group-auth-checker.ts | 13 +++++---- backend/src/services/students.ts | 3 +- backend/src/services/teachers.ts | 3 +- backend/tests/controllers/teachers.test.ts | 5 ++-- 7 files changed, 38 insertions(+), 36 deletions(-) diff --git a/backend/src/interfaces/user.ts b/backend/src/interfaces/user.ts index f4413b5e..88252a1e 100644 --- a/backend/src/interfaces/user.ts +++ b/backend/src/interfaces/user.ts @@ -10,6 +10,10 @@ export function mapToUserDTO(user: User): UserDTO { }; } +export function mapToUsername(user: {username: string}): string { + return user.username; +} + export function mapToUser(userData: UserDTO, userInstance: T): T { userInstance.username = userData.username; userInstance.firstName = userData.firstName; diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts index a9bbe87f..e9b284c4 100644 --- a/backend/src/middleware/auth/checks/assignment-auth-checks.ts +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -1,7 +1,8 @@ import {authorize} from "./auth-checks"; -import {getAssignment} from "../../../services/assignments"; -import {getClass} from "../../../services/classes"; +import {fetchAssignment, getAssignment} from "../../../services/assignments"; +import {fetchClass, getClass} from "../../../services/classes"; import {getAllGroups} from "../../../services/groups"; +import {mapToUsername} from "../../../interfaces/user"; /** * Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment). @@ -12,15 +13,13 @@ import {getAllGroups} from "../../../services/groups"; export const onlyAllowIfHasAccessToAssignment = authorize( async (auth, req) => { const { classid: classId, id: assignmentId } = req.params as { classid: string, id: number }; - const assignment = await getAssignment(classId, assignmentId); - if (assignment === null) { - return false; - } else if (auth.accountType === "teacher") { - const clazz = await getClass(assignment.class); - return auth.username in clazz!.teachers; + const assignment = await fetchAssignment(classId, assignmentId); + if (auth.accountType === "teacher") { + const clazz = await fetchClass(assignment.class); + return clazz.teachers.map(mapToUsername).includes(auth.username); } else { const groups = await getAllGroups(classId, assignmentId, false); - return groups.some(group => auth.username in (group.members as string[])); + return groups.some(group => group.members.map(mapToUsername).includes(auth.username) ); } } ); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index c89a263f..1f8e685b 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -1,11 +1,12 @@ import {authorize} from "./auth-checks"; import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; -import {getClass} from "../../../services/classes"; +import {fetchClass, getClass} from "../../../services/classes"; +import {mapToUsername} from "../../../interfaces/user"; async function teaches(teacherUsername: string, classId: string): Promise { - const clazz = await getClass(classId); - return clazz !== null && teacherUsername in clazz.teachers; + const clazz = await fetchClass(classId); + return clazz.teachers.map(mapToUsername).includes(teacherUsername); } /** @@ -20,7 +21,7 @@ export const onlyAllowStudentHimselfAndTeachersOfClass = authorize( } else if (auth.accountType === "teacher") { return teaches(auth.username, req.params.classId); } - return false; + return false; } ); @@ -41,13 +42,11 @@ export const onlyAllowTeacherOfClass = authorize( export const onlyAllowIfInClass = authorize( async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const classId = req.params.classId ?? req.params.classid ?? req.params.id; - const clazz = await getClass(classId); - if (clazz === null) { - return false; - } else if (auth.accountType === "teacher") { - return auth.username in clazz.teachers; + const clazz = await fetchClass(classId); + if (auth.accountType === "teacher") { + return clazz.teachers.map(mapToUsername).includes(auth.username); } - return auth.username in clazz.students; + return clazz.students.map(mapToUsername).includes(auth.username); } ); @@ -57,13 +56,11 @@ export const onlyAllowIfInClass = authorize( export const onlyAllowOwnClassInBody = authorize( async (auth, req) => { const classId = (req.body as {class: string})?.class; - const clazz = await getClass(classId); + const clazz = await fetchClass(classId); - if (clazz === null) { - return false; - } else if (auth.accountType === "teacher") { - return auth.username in clazz.teachers; + if (auth.accountType === "teacher") { + return clazz.teachers.map(mapToUsername).includes(auth.username); } - return auth.username in clazz.students; + return clazz.students.map(mapToUsername).includes(auth.username); } ); diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts index f62ab3b9..0f42741e 100644 --- a/backend/src/middleware/auth/checks/group-auth-checker.ts +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -1,6 +1,7 @@ import {authorize} from "./auth-checks"; -import {getClass} from "../../../services/classes"; -import {getGroup} from "../../../services/groups"; +import {fetchClass, getClass} from "../../../services/classes"; +import {fetchGroup, getGroup} from "../../../services/groups"; +import {mapToUsername} from "../../../interfaces/user"; /** * Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'. @@ -14,11 +15,11 @@ export const onlyAllowIfHasAccessToGroup = authorize( req.params as { classid: string, assignmentid: number, groupid: number }; if (auth.accountType === "teacher") { - const clazz = await getClass(classId); - return auth.username in clazz!.teachers; + const clazz = await fetchClass(classId); + return clazz.teachers.map(mapToUsername).includes(auth.username); } else { // user is student - const group = await getGroup(classId, assignmentId, groupId, false); - return group === null ? false : auth.username in (group.members as string[]); + const group = await fetchGroup(classId, assignmentId, groupId, false); + return clazz.students.map(mapToUsername).includes(auth.username); } } ); diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 31dee851..7101960f 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -25,6 +25,7 @@ import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; import { Submission } from '../entities/assignments/submission.entity'; +import {mapToUsername} from "../interfaces/user"; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); @@ -34,7 +35,7 @@ export async function getAllStudents(full: boolean): Promise user.username); + return users.map(mapToUsername); } export async function fetchStudent(username: string): Promise { diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index a0e6c7a7..a76f88b7 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -30,6 +30,7 @@ import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; +import {mapToUsername} from "../interfaces/user"; export async function getAllTeachers(full: boolean): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); @@ -38,7 +39,7 @@ export async function getAllTeachers(full: boolean): Promise user.username); + return users.map(mapToUsername); } export async function fetchTeacher(username: string): Promise { diff --git a/backend/tests/controllers/teachers.test.ts b/backend/tests/controllers/teachers.test.ts index a73a79a5..39e59d6f 100644 --- a/backend/tests/controllers/teachers.test.ts +++ b/backend/tests/controllers/teachers.test.ts @@ -96,7 +96,7 @@ describe('Teacher controllers', () => { }); it('Teacher list', async () => { - req = { query: { full: 'true' } }; + req = { query: { full: 'false' } }; await getAllTeachersHandler(req as Request, res as Response); @@ -104,8 +104,7 @@ describe('Teacher controllers', () => { const result = jsonMock.mock.lastCall?.[0]; - const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username); - expect(teacherUsernames).toContain('testleerkracht1'); + expect(result.teachers).toContain('testleerkracht1'); expect(result.teachers).toHaveLength(5); }); From 9102268be1f9af14b9e452eb069f05ae3860bda6 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 15:34:37 +0200 Subject: [PATCH 011/117] feat: assignment permissies geupdate --- .../auth/checks/assignment-auth-checks.ts | 13 +++++------ backend/src/routes/assignments.ts | 22 +++++-------------- backend/src/services/groups.ts | 9 ++++++++ 3 files changed, 21 insertions(+), 23 deletions(-) diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts index e9b284c4..11199bb1 100644 --- a/backend/src/middleware/auth/checks/assignment-auth-checks.ts +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -1,7 +1,7 @@ import {authorize} from "./auth-checks"; -import {fetchAssignment, getAssignment} from "../../../services/assignments"; -import {fetchClass, getClass} from "../../../services/classes"; -import {getAllGroups} from "../../../services/groups"; +import {fetchAssignment} from "../../../services/assignments"; +import {fetchClass} from "../../../services/classes"; +import {fetchAllGroups} from "../../../services/groups"; import {mapToUsername} from "../../../interfaces/user"; /** @@ -13,13 +13,12 @@ import {mapToUsername} from "../../../interfaces/user"; export const onlyAllowIfHasAccessToAssignment = authorize( async (auth, req) => { const { classid: classId, id: assignmentId } = req.params as { classid: string, id: number }; - const assignment = await fetchAssignment(classId, assignmentId); if (auth.accountType === "teacher") { - const clazz = await fetchClass(assignment.class); + const clazz = await fetchClass(classId); return clazz.teachers.map(mapToUsername).includes(auth.username); } else { - const groups = await getAllGroups(classId, assignmentId, false); - return groups.some(group => group.members.map(mapToUsername).includes(auth.username) ); + const groups = await fetchAllGroups(classId, assignmentId); + return groups.some(group => group.members.map((member) => member.username).includes(auth.username) ); } } ); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index 5173a274..8bf42022 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -9,32 +9,22 @@ import { } from '../controllers/assignments.js'; import groupRouter from './groups.js'; import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowOwnClassInBody} from "../middleware/auth/checks/class-auth-checks"; +import {onlyAllowIfInClass, onlyAllowOwnClassInBody} from "../middleware/auth/checks/class-auth-checks"; import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks"; const router = express.Router({ mergeParams: true }); -router.get('/', getAllAssignmentsHandler); -// Root endpoint used to search objects -router.get('/', adminOnly, getAllAssignmentsHandler); +router.get('/', teachersOnly, onlyAllowIfInClass, getAllAssignmentsHandler); -router.post('/', teachersOnly, onlyAllowOwnClassInBody, createAssignmentHandler); +router.post('/', teachersOnly, onlyAllowIfInClass, createAssignmentHandler); -router.get('/:id', getAssignmentHandler); -// Information about an assignment with id 'id' router.get('/:id', onlyAllowIfHasAccessToAssignment, getAssignmentHandler); -router.put('/:id', putAssignmentHandler); +router.put('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, putAssignmentHandler); -router.delete('/:id', deleteAssignmentHandler); +router.delete('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteAssignmentHandler); -router.get('/:id/submissions', onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler); - -router.get('/:id/questions', onlyAllowIfHasAccessToAssignment, (_req, res) => { - res.json({ - questions: ['0'], - }); -}); +router.get('/:id/submissions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler); router.use('/:assignmentid/groups', groupRouter); diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 3c6f2919..b94d435c 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -22,6 +22,15 @@ export async function fetchGroup(classId: string, assignmentNumber: number, grou return group; } +export async function fetchAllGroups(classId: string, assignmentNumber: number): Promise { + const assignment = await fetchAssignment(classId, assignmentNumber); + + const groupRepository = getGroupRepository(); + const groups = await groupRepository.findAllGroupsForAssignment(assignment); + + return groups; +} + export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number): Promise { const group = await fetchGroup(classId, assignmentNumber, groupNumber); return mapToGroupDTO(group); From 7c41c8e615cabe62d3530bd06661fe7ae54bf17e Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 15:37:22 +0200 Subject: [PATCH 012/117] feat: class permissies geupdate --- backend/src/routes/classes.ts | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index 640d8513..9cf20ec0 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -19,31 +19,30 @@ import {onlyAllowIfInClass} from "../middleware/auth/checks/class-auth-checks"; const router = express.Router(); -// Root endpoint used to search objects router.get('/', adminOnly, getAllClassesHandler); router.post('/', teachersOnly, createClassHandler); -// Information about an class with id 'id' router.get('/:id', onlyAllowIfInClass, getClassHandler); -router.put('/:id', putClassHandler); +router.put('/:id', teachersOnly, onlyAllowIfInClass, putClassHandler); -router.delete('/:id', deleteClassHandler); +router.delete('/:id', teachersOnly, onlyAllowIfInClass, deleteClassHandler); router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler); router.get('/:id/students', onlyAllowIfInClass, getClassStudentsHandler); -router.post('/:id/students', addClassStudentHandler); +router.post('/:id/students', teachersOnly, onlyAllowIfInClass, addClassStudentHandler); -router.delete('/:id/students/:username', deleteClassStudentHandler); +router.delete('/:id/students/:username', teachersOnly, onlyAllowIfInClass, deleteClassStudentHandler); -router.get('/:id/teachers', getClassTeachersHandler); +router.get('/:id/teachers', onlyAllowIfInClass, getClassTeachersHandler); -router.post('/:id/teachers', addClassTeacherHandler); +// De combinatie van deze POST en DELETE endpoints kan lethal zijn +router.post('/:id/teachers', teachersOnly, onlyAllowIfInClass, addClassTeacherHandler); -router.delete('/:id/teachers/:username', deleteClassTeacherHandler); +router.delete('/:id/teachers/:username', teachersOnly, onlyAllowIfInClass, deleteClassTeacherHandler); router.use('/:classid/assignments', assignmentRouter); From a4ccae6c0d7a32d3b259b4245d1fa852bd19b745 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 16:47:31 +0200 Subject: [PATCH 013/117] feat: authenticatie voor submissions en groups toegevoegd --- .../auth/checks/group-auth-checker.ts | 4 +-- .../middleware/auth/checks/question-checks.ts | 8 ++--- .../auth/checks/submission-checks.ts | 29 +++++++++++++++++++ backend/src/routes/groups.ts | 8 ++--- backend/src/routes/submissions.ts | 13 +++++---- 5 files changed, 45 insertions(+), 17 deletions(-) create mode 100644 backend/src/middleware/auth/checks/submission-checks.ts diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts index 0f42741e..73df4fb5 100644 --- a/backend/src/middleware/auth/checks/group-auth-checker.ts +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -18,8 +18,8 @@ export const onlyAllowIfHasAccessToGroup = authorize( const clazz = await fetchClass(classId); return clazz.teachers.map(mapToUsername).includes(auth.username); } else { // user is student - const group = await fetchGroup(classId, assignmentId, groupId, false); - return clazz.students.map(mapToUsername).includes(auth.username); + const group = await fetchGroup(classId, assignmentId, groupId); + return group.members.map(mapToUsername).includes(auth.username); } } ); diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts index bfe76061..c83e1de2 100644 --- a/backend/src/middleware/auth/checks/question-checks.ts +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -9,11 +9,11 @@ import {fetchAnswer} from "../../../services/answers"; import {mapToUsername} from "../../../interfaces/user"; export const onlyAllowAuthor = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.body.author === auth.username + (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username ); export const onlyAllowAuthorRequest = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; @@ -30,7 +30,7 @@ export const onlyAllowAuthorRequest = authorize( ); export const onlyAllowAuthorRequestAnswer = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; @@ -49,7 +49,7 @@ export const onlyAllowAuthorRequestAnswer = authorize( ); export const onlyAllowIfHasAccessToQuestion = authorize( - async (auth, req) => { + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const hruid = req.params.hruid; const version = req.params.version; const language = req.query.lang as string; diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts new file mode 100644 index 00000000..78087fa9 --- /dev/null +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -0,0 +1,29 @@ +import { languageMap } from "dwengo-1-common/util/language"; +import { LearningObjectIdentifier } from "../../../entities/content/learning-object-identifier"; +import { fetchSubmission } from "../../../services/submissions"; +import { AuthenticatedRequest } from "../authenticated-request"; +import { AuthenticationInfo } from "../authentication-info"; +import { authorize } from "./auth-checks"; +import { FALLBACK_LANG } from "../../../config"; +import { mapToUsername } from "../../../interfaces/user"; + +export const onlyAllowSubmitter = authorize( + (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username +); + +export const onlyAllowIfHasAccessToSubmission = authorize( + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const { hruid: lohruid, id: submissionNumber } = req.params; + const { language: lang, version: version } = req.query; + + const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version)) + const submission = await fetchSubmission(loId, Number(submissionNumber)); + + if (auth.accountType === "teacher") { + // Dit kan niet werken om dat al deze objecten niet gepopulate zijn. + return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username); + } + + return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username); + } +) \ No newline at end of file diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 8264e584..5d3d8ed0 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -13,17 +13,15 @@ import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assign const router = express.Router({ mergeParams: true }); -// Root endpoint used to search objects router.get('/', onlyAllowIfHasAccessToAssignment, getAllGroupsHandler); router.post('/', teachersOnly, onlyAllowIfHasAccessToAssignment, createGroupHandler); -// Information about a group (members, ... [TODO DOC]) -router.get('/:groupid', onlyAllowIfHasAccessToGroup, getGroupHandler); +router.get('/:groupid', onlyAllowIfHasAccessToAssignment, getGroupHandler); -router.put('/:groupid', putGroupHandler); +router.put('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, putGroupHandler); -router.delete('/:groupid', deleteGroupHandler); +router.delete('/:groupid', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteGroupHandler); router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler); diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 492b6439..8030f9f8 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -1,15 +1,16 @@ import express from 'express'; import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.js'; +import { onlyAllowAuthor } from '../middleware/auth/checks/question-checks.js'; +import { onlyAllowIfHasAccessToSubmission, onlyAllowSubmitter } from '../middleware/auth/checks/submission-checks.js'; +import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router({ mergeParams: true }); -// Root endpoint used to search objects -router.get('/', getSubmissionsHandler); +router.get('/', adminOnly, getSubmissionsHandler); -router.post('/:id', createSubmissionHandler); +router.post('/:id', studentsOnly, onlyAllowSubmitter, createSubmissionHandler); -// Information about an submission with id 'id' -router.get('/:id', getSubmissionHandler); +router.get('/:id', onlyAllowIfHasAccessToSubmission, getSubmissionHandler); -router.delete('/:id', deleteSubmissionHandler); +router.delete('/:id', onlyAllowIfHasAccessToSubmission, deleteSubmissionHandler); export default router; From b4b9abcc487716be1e9f97c56e8b4beb5c361992 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 16:56:03 +0200 Subject: [PATCH 014/117] fix: fixed syntax & typescript errors --- .../auth/checks/learning-content-auth-checks.ts | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts index 57a3021d..64d78c73 100644 --- a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts +++ b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts @@ -1,7 +1,7 @@ import {authorize} from "./auth-checks"; import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; -import {getGroup} from "../../../services/groups"; +import {fetchGroup, getGroup} from "../../../services/groups"; /** * Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId') @@ -13,9 +13,10 @@ import {getGroup} from "../../../services/groups"; export const onlyAllowPersonalizationForOwnGroup = authorize( async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const {forGroup, assignmentNo, classId} = req.params; - if (forGroup && assignmentNo && classId) { - const group = getGroup(forGroup, parseInt(assignmentNo), classId, false); - + if (auth.accountType === "student" && forGroup && assignmentNo && classId) { + // TODO: groupNumber? + // const group = await fetchGroup(Number(classId), Number(assignmentNo), ) + return false; } else { return true; } From 356d4aafaddf3d4ffddfb32d2c7102ec64100295 Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 17:33:22 +0200 Subject: [PATCH 015/117] feat: assignment's question endpoint authenticatie --- backend/src/routes/assignments.ts | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index 8bf42022..7b2d66fa 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -8,8 +8,8 @@ import { putAssignmentHandler, } from '../controllers/assignments.js'; import groupRouter from './groups.js'; -import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowIfInClass, onlyAllowOwnClassInBody} from "../middleware/auth/checks/class-auth-checks"; +import {teachersOnly} from "../middleware/auth/checks/auth-checks"; +import {onlyAllowIfInClass} from "../middleware/auth/checks/class-auth-checks"; import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks"; const router = express.Router({ mergeParams: true }); @@ -26,6 +26,12 @@ router.delete('/:id', teachersOnly, onlyAllowIfHasAccessToAssignment, deleteAssi router.get('/:id/submissions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler); +router.get('/:id/questions', teachersOnly, onlyAllowIfHasAccessToAssignment, (_req, res) => { + res.json({ + questions: ['0'], + }); +}); + router.use('/:assignmentid/groups', groupRouter); export default router; From 7f670030a762ef2e83b8439ab63971082019bbfe Mon Sep 17 00:00:00 2001 From: Adriaan Jacquet Date: Tue, 22 Apr 2025 17:57:18 +0200 Subject: [PATCH 016/117] fix: fixed linter errors --- .../src/middleware/auth/checks/assignment-auth-checks.ts | 5 ++--- backend/src/middleware/auth/checks/class-auth-checks.ts | 2 +- backend/src/middleware/auth/checks/group-auth-checker.ts | 8 ++++---- .../auth/checks/learning-content-auth-checks.ts | 7 +++---- backend/src/middleware/auth/checks/question-checks.ts | 8 ++++---- .../middleware/auth/checks/teacher-invitation-checks.ts | 4 ++-- backend/src/routes/answers.ts | 2 +- backend/src/routes/questions.ts | 2 +- backend/src/services/questions.ts | 2 +- backend/src/services/students.ts | 3 ++- backend/src/services/teachers.ts | 3 ++- backend/tests/controllers/teachers.test.ts | 1 - frontend/src/services/auth/auth-service.ts | 4 ---- 13 files changed, 23 insertions(+), 28 deletions(-) diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts index 11199bb1..070df4a0 100644 --- a/backend/src/middleware/auth/checks/assignment-auth-checks.ts +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -1,5 +1,4 @@ import {authorize} from "./auth-checks"; -import {fetchAssignment} from "../../../services/assignments"; import {fetchClass} from "../../../services/classes"; import {fetchAllGroups} from "../../../services/groups"; import {mapToUsername} from "../../../interfaces/user"; @@ -16,9 +15,9 @@ export const onlyAllowIfHasAccessToAssignment = authorize( if (auth.accountType === "teacher") { const clazz = await fetchClass(classId); return clazz.teachers.map(mapToUsername).includes(auth.username); - } else { + } const groups = await fetchAllGroups(classId, assignmentId); return groups.some(group => group.members.map((member) => member.username).includes(auth.username) ); - } + } ); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 1f8e685b..603142be 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -1,7 +1,7 @@ import {authorize} from "./auth-checks"; import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; -import {fetchClass, getClass} from "../../../services/classes"; +import {fetchClass} from "../../../services/classes"; import {mapToUsername} from "../../../interfaces/user"; async function teaches(teacherUsername: string, classId: string): Promise { diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts index 73df4fb5..643a3713 100644 --- a/backend/src/middleware/auth/checks/group-auth-checker.ts +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -1,6 +1,6 @@ import {authorize} from "./auth-checks"; -import {fetchClass, getClass} from "../../../services/classes"; -import {fetchGroup, getGroup} from "../../../services/groups"; +import {fetchClass} from "../../../services/classes"; +import {fetchGroup} from "../../../services/groups"; import {mapToUsername} from "../../../interfaces/user"; /** @@ -17,9 +17,9 @@ export const onlyAllowIfHasAccessToGroup = authorize( if (auth.accountType === "teacher") { const clazz = await fetchClass(classId); return clazz.teachers.map(mapToUsername).includes(auth.username); - } else { // user is student + } // User is student const group = await fetchGroup(classId, assignmentId, groupId); return group.members.map(mapToUsername).includes(auth.username); - } + } ); diff --git a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts index 64d78c73..a13cd038 100644 --- a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts +++ b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts @@ -1,7 +1,6 @@ import {authorize} from "./auth-checks"; import {AuthenticationInfo} from "../authentication-info"; import {AuthenticatedRequest} from "../authenticated-request"; -import {fetchGroup, getGroup} from "../../../services/groups"; /** * Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId') @@ -15,10 +14,10 @@ export const onlyAllowPersonalizationForOwnGroup = authorize( const {forGroup, assignmentNo, classId} = req.params; if (auth.accountType === "student" && forGroup && assignmentNo && classId) { // TODO: groupNumber? - // const group = await fetchGroup(Number(classId), Number(assignmentNo), ) + // Const group = await fetchGroup(Number(classId), Number(assignmentNo), ) return false; - } else { + } return true; - } + } ); diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts index c83e1de2..38b1f0ef 100644 --- a/backend/src/middleware/auth/checks/question-checks.ts +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -25,7 +25,7 @@ export const onlyAllowAuthorRequest = authorize( const question = await fetchQuestion(questionId); - return question.author.username == auth.username; + return question.author.username === auth.username; } ); @@ -44,7 +44,7 @@ export const onlyAllowAuthorRequestAnswer = authorize( const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; const answer = await fetchAnswer(questionId, sequenceNumber); - return answer.author.username == auth.username; + return answer.author.username === auth.username; } ); @@ -65,8 +65,8 @@ export const onlyAllowIfHasAccessToQuestion = authorize( if (auth.accountType === "teacher") { const cls = group.assignment.within; // TODO check if contains full objects return cls.teachers.map(mapToUsername).includes(auth.username); - } else { // user is student + } // User is student return group.members.map(mapToUsername).includes(auth.username); - } + } ); diff --git a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts index 6ebc8512..27d0cab2 100644 --- a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts +++ b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts @@ -14,10 +14,10 @@ export const onlyAllowSender = authorize( export const onlyAllowSenderBody = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => - req.body.sender === auth.username + (req.body as { sender: string }).sender === auth.username ); export const onlyAllowReceiverBody = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => - req.body.receiver === auth.username + (req.body as { receiver: string }).receiver === auth.username ); diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index 2571c56d..a5ee6278 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,6 +1,6 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; -import {adminOnly, authenticatedOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; +import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 1363ae1a..bffcbe9e 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,7 +1,7 @@ import express from 'express'; import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; -import {adminOnly, authenticatedOnly, studentsOnly} from "../middleware/auth/checks/auth-checks"; +import {adminOnly, studentsOnly} from "../middleware/auth/checks/auth-checks"; import {updateAnswerHandler} from "../controllers/answers"; import { onlyAllowAuthor, diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 01e4cc5c..70400226 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -102,7 +102,7 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat const question = await questionRepository.createQuestion({ loId, author, - inGroup: group!, + inGroup: group, content, }); diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 95e836a0..09ba1643 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -59,7 +59,8 @@ export async function getStudent(username: string): Promise { return mapToStudentDTO(user); } -export async function createStudent(userData: StudentDTO, allowUpdate = false): Promise { +// TODO allowupdate parameter? +export async function createStudent(userData: StudentDTO, _allowUpdate = false): Promise { const studentRepository = getStudentRepository(); const newStudent = mapToStudent(userData); diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 12148ebd..79072616 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -58,7 +58,8 @@ export async function getTeacher(username: string): Promise { return mapToTeacherDTO(user); } -export async function createTeacher(userData: TeacherDTO, update?: boolean): Promise { +// TODO update parameter +export async function createTeacher(userData: TeacherDTO, _update?: boolean): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); const newTeacher = mapToTeacher(userData); diff --git a/backend/tests/controllers/teachers.test.ts b/backend/tests/controllers/teachers.test.ts index 39e59d6f..26a2d56c 100644 --- a/backend/tests/controllers/teachers.test.ts +++ b/backend/tests/controllers/teachers.test.ts @@ -15,7 +15,6 @@ import { import { BadRequestException } from '../../src/exceptions/bad-request-exception.js'; import { EntityAlreadyExistsException } from '../../src/exceptions/entity-already-exists-exception.js'; import { getStudentRequestsHandler } from '../../src/controllers/students.js'; -import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { getClassHandler } from '../../src/controllers/classes'; describe('Teacher controllers', () => { diff --git a/frontend/src/services/auth/auth-service.ts b/frontend/src/services/auth/auth-service.ts index 7b0ff5ee..a813cd6e 100644 --- a/frontend/src/services/auth/auth-service.ts +++ b/frontend/src/services/auth/auth-service.ts @@ -29,10 +29,6 @@ const authState = reactive({ activeRole: authStorage.getActiveRole() ?? null, }); -async function sendHello(): Promise { - return apiClient.post("/auth/hello"); -} - /** * Load the information about who is currently logged in from the IDP. */ From 0c47546814ad856a534611c76d2ea33836e466dc Mon Sep 17 00:00:00 2001 From: Lint Action Date: Tue, 22 Apr 2025 16:04:52 +0000 Subject: [PATCH 017/117] style: fix linting issues met Prettier --- backend/src/controllers/auth.ts | 40 ++++--- .../src/controllers/teacher-invitations.ts | 6 +- backend/src/interfaces/user.ts | 2 +- backend/src/middleware/auth/auth.ts | 10 +- .../auth/authenticated-request.d.ts | 4 +- .../auth/checks/assignment-auth-checks.ts | 27 ++--- .../src/middleware/auth/checks/auth-checks.ts | 26 +++-- .../auth/checks/class-auth-checks.ts | 62 +++++------ .../auth/checks/group-auth-checker.ts | 34 +++--- .../checks/learning-content-auth-checks.ts | 25 ++--- .../middleware/auth/checks/question-checks.ts | 105 ++++++++---------- .../auth/checks/submission-checks.ts | 40 ++++--- .../auth/checks/teacher-invitation-checks.ts | 20 ++-- .../auth/checks/user-auth-checks.ts | 10 +- backend/src/routes/answers.ts | 9 +- backend/src/routes/assignments.ts | 6 +- backend/src/routes/classes.ts | 4 +- backend/src/routes/groups.ts | 6 +- backend/src/routes/learning-objects.ts | 2 +- backend/src/routes/learning-paths.ts | 2 +- backend/src/routes/questions.ts | 12 +- backend/src/routes/student-join-requests.ts | 4 +- backend/src/routes/students.ts | 4 +- backend/src/routes/teacher-invitations.ts | 11 +- backend/src/routes/teachers.ts | 6 +- backend/src/routes/themes.ts | 2 +- backend/src/services/questions.ts | 10 +- backend/src/services/students.ts | 2 +- backend/src/services/teacher-invitations.ts | 2 +- backend/src/services/teachers.ts | 2 +- 30 files changed, 233 insertions(+), 262 deletions(-) diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index 14b05dc8..6395715d 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -2,10 +2,10 @@ import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; import { getLogger } from '../logging/initalize.js'; import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; import { envVars, getEnvVar } from '../util/envVars.js'; -import {createOrUpdateStudent, createStudent} from "../services/students"; -import {AuthenticationInfo} from "../middleware/auth/authentication-info"; -import {Request, Response} from "express"; -import {createOrUpdateTeacher, createTeacher} from "../services/teachers"; +import { createOrUpdateStudent, createStudent } from '../services/students'; +import { AuthenticationInfo } from '../middleware/auth/authentication-info'; +import { Request, Response } from 'express'; +import { createOrUpdateTeacher, createTeacher } from '../services/teachers'; interface FrontendIdpConfig { authority: string; @@ -47,20 +47,26 @@ export function handleGetFrontendAuthConfig(_req: Request, res: Response): void export async function handleHello(req: AuthenticatedRequest): Promise { const auth: AuthenticationInfo = req.auth!; - if (auth.accountType === "teacher") { - await createTeacher({ - id: auth.username, - username: auth.username, - firstName: auth.firstName ?? "", - lastName: auth.lastName ?? "", - }, true); + if (auth.accountType === 'teacher') { + await createTeacher( + { + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? '', + lastName: auth.lastName ?? '', + }, + true + ); } else { - await createStudent({ - id: auth.username, - username: auth.username, - firstName: auth.firstName ?? "", - lastName: auth.lastName ?? "", - }, true); + await createStudent( + { + id: auth.username, + username: auth.username, + firstName: auth.firstName ?? '', + lastName: auth.lastName ?? '', + }, + true + ); } } diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index 0a8b89c0..f1350660 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { requireFields } from './error-helper.js'; import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; -import {ConflictException} from "../exceptions/conflict-exception"; +import { ConflictException } from '../exceptions/conflict-exception'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; @@ -31,8 +31,8 @@ export async function createInvitationHandler(req: Request, res: Response): Prom const classId = req.body.class; requireFields({ sender, receiver, classId }); - if (sender === receiver){ - throw new ConflictException("Cannot send an invitation to yourself"); + if (sender === receiver) { + throw new ConflictException('Cannot send an invitation to yourself'); } const data = req.body as TeacherInvitationData; diff --git a/backend/src/interfaces/user.ts b/backend/src/interfaces/user.ts index 88252a1e..3084c494 100644 --- a/backend/src/interfaces/user.ts +++ b/backend/src/interfaces/user.ts @@ -10,7 +10,7 @@ export function mapToUserDTO(user: User): UserDTO { }; } -export function mapToUsername(user: {username: string}): string { +export function mapToUsername(user: { username: string }): string { return user.username; } diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 158e2c28..24be4825 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -1,11 +1,11 @@ -import {envVars, getEnvVar} from '../../util/envVars.js'; -import {expressjwt} from 'express-jwt'; +import { envVars, getEnvVar } from '../../util/envVars.js'; +import { expressjwt } from 'express-jwt'; import * as jwt from 'jsonwebtoken'; -import {JwtPayload} from 'jsonwebtoken'; +import { JwtPayload } from 'jsonwebtoken'; import jwksClient from 'jwks-rsa'; import * as express from 'express'; -import {AuthenticatedRequest} from './authenticated-request.js'; -import {AuthenticationInfo} from './authentication-info.js'; +import { AuthenticatedRequest } from './authenticated-request.js'; +import { AuthenticationInfo } from './authentication-info.js'; import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; const JWKS_CACHE = true; diff --git a/backend/src/middleware/auth/authenticated-request.d.ts b/backend/src/middleware/auth/authenticated-request.d.ts index c0049dc7..af7630af 100644 --- a/backend/src/middleware/auth/authenticated-request.d.ts +++ b/backend/src/middleware/auth/authenticated-request.d.ts @@ -1,7 +1,7 @@ import { Request } from 'express'; import { JwtPayload } from 'jsonwebtoken'; import { AuthenticationInfo } from './authentication-info.js'; -import * as core from "express-serve-static-core"; +import * as core from 'express-serve-static-core'; export interface AuthenticatedRequest< P = core.ParamsDictionary, @@ -9,7 +9,7 @@ export interface AuthenticatedRequest< ReqBody = unknown, ReqQuery = core.Query, Locals extends Record = Record, -> extends Request { +> extends Request { // Properties are optional since the user is not necessarily authenticated. jwtPayload?: JwtPayload; auth?: AuthenticationInfo; diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts index 070df4a0..8328dec0 100644 --- a/backend/src/middleware/auth/checks/assignment-auth-checks.ts +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -1,7 +1,7 @@ -import {authorize} from "./auth-checks"; -import {fetchClass} from "../../../services/classes"; -import {fetchAllGroups} from "../../../services/groups"; -import {mapToUsername} from "../../../interfaces/user"; +import { authorize } from './auth-checks'; +import { fetchClass } from '../../../services/classes'; +import { fetchAllGroups } from '../../../services/groups'; +import { mapToUsername } from '../../../interfaces/user'; /** * Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment). @@ -9,15 +9,12 @@ import {mapToUsername} from "../../../interfaces/user"; * - either teachers of the class the assignment was posted in, * - or students in a group of the assignment. */ -export const onlyAllowIfHasAccessToAssignment = authorize( - async (auth, req) => { - const { classid: classId, id: assignmentId } = req.params as { classid: string, id: number }; - if (auth.accountType === "teacher") { - const clazz = await fetchClass(classId); - return clazz.teachers.map(mapToUsername).includes(auth.username); - } - const groups = await fetchAllGroups(classId, assignmentId); - return groups.some(group => group.members.map((member) => member.username).includes(auth.username) ); - +export const onlyAllowIfHasAccessToAssignment = authorize(async (auth, req) => { + const { classid: classId, id: assignmentId } = req.params as { classid: string; id: number }; + if (auth.accountType === 'teacher') { + const clazz = await fetchClass(classId); + return clazz.teachers.map(mapToUsername).includes(auth.username); } -); + const groups = await fetchAllGroups(classId, assignmentId); + return groups.some((group) => group.members.map((member) => member.username).includes(auth.username)); +}); diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts index d14e8dee..a4a75db5 100644 --- a/backend/src/middleware/auth/checks/auth-checks.ts +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -1,9 +1,9 @@ -import {AuthenticationInfo} from "../authentication-info"; -import {AuthenticatedRequest} from "../authenticated-request"; -import * as express from "express"; -import {UnauthorizedException} from "../../../exceptions/unauthorized-exception"; -import {ForbiddenException} from "../../../exceptions/forbidden-exception"; -import {RequestHandler} from "express"; +import { AuthenticationInfo } from '../authentication-info'; +import { AuthenticatedRequest } from '../authenticated-request'; +import * as express from 'express'; +import { UnauthorizedException } from '../../../exceptions/unauthorized-exception'; +import { ForbiddenException } from '../../../exceptions/forbidden-exception'; +import { RequestHandler } from 'express'; /** * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill @@ -11,13 +11,17 @@ import {RequestHandler} from "express"; * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * to true. */ -export function authorize>( - accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise -): RequestHandler { - return async (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): Promise => { +export function authorize>( + accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise +): RequestHandler { + return async ( + req: AuthenticatedRequest, + _res: express.Response, + next: express.NextFunction + ): Promise => { if (!req.auth) { throw new UnauthorizedException(); - } else if (!await accessCondition(req.auth, req)) { + } else if (!(await accessCondition(req.auth, req))) { throw new ForbiddenException(); } else { next(); diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 603142be..6aae97b3 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -1,8 +1,8 @@ -import {authorize} from "./auth-checks"; -import {AuthenticationInfo} from "../authentication-info"; -import {AuthenticatedRequest} from "../authenticated-request"; -import {fetchClass} from "../../../services/classes"; -import {mapToUsername} from "../../../interfaces/user"; +import { authorize } from './auth-checks'; +import { AuthenticationInfo } from '../authentication-info'; +import { AuthenticatedRequest } from '../authenticated-request'; +import { fetchClass } from '../../../services/classes'; +import { mapToUsername } from '../../../interfaces/user'; async function teaches(teacherUsername: string, classId: string): Promise { const clazz = await fetchClass(classId); @@ -14,53 +14,45 @@ async function teaches(teacherUsername: string, classId: string): Promise { - if (req.params.username === auth.username) { - return true; - } else if (auth.accountType === "teacher") { - return teaches(auth.username, req.params.classId); - } - return false; - +export const onlyAllowStudentHimselfAndTeachersOfClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + if (req.params.username === auth.username) { + return true; + } else if (auth.accountType === 'teacher') { + return teaches(auth.username, req.params.classId); } -); + return false; +}); /** * Only let the request pass through if its path parameter "username" is the username of the currently logged-in * teacher and the path parameter "classId" refers to a class the teacher teaches. */ export const onlyAllowTeacherOfClass = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => - req.params.username === auth.username && teaches(auth.username, req.params.classId), + async (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username && teaches(auth.username, req.params.classId) ); /** * Only let the request pass through if the class id in it refers to a class the current user is in (as a student * or teacher) */ -export const onlyAllowIfInClass = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const classId = req.params.classId ?? req.params.classid ?? req.params.id; - const clazz = await fetchClass(classId); - if (auth.accountType === "teacher") { - return clazz.teachers.map(mapToUsername).includes(auth.username); - } - return clazz.students.map(mapToUsername).includes(auth.username); +export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const classId = req.params.classId ?? req.params.classid ?? req.params.id; + const clazz = await fetchClass(classId); + if (auth.accountType === 'teacher') { + return clazz.teachers.map(mapToUsername).includes(auth.username); } -); + return clazz.students.map(mapToUsername).includes(auth.username); +}); /** * Only allows the request to pass if the 'class' property in its body is a class the current user is a member of. */ -export const onlyAllowOwnClassInBody = authorize( - async (auth, req) => { - const classId = (req.body as {class: string})?.class; - const clazz = await fetchClass(classId); +export const onlyAllowOwnClassInBody = authorize(async (auth, req) => { + const classId = (req.body as { class: string })?.class; + const clazz = await fetchClass(classId); - if (auth.accountType === "teacher") { - return clazz.teachers.map(mapToUsername).includes(auth.username); - } - return clazz.students.map(mapToUsername).includes(auth.username); + if (auth.accountType === 'teacher') { + return clazz.teachers.map(mapToUsername).includes(auth.username); } -); + return clazz.students.map(mapToUsername).includes(auth.username); +}); diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts index 643a3713..bebda954 100644 --- a/backend/src/middleware/auth/checks/group-auth-checker.ts +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -1,7 +1,7 @@ -import {authorize} from "./auth-checks"; -import {fetchClass} from "../../../services/classes"; -import {fetchGroup} from "../../../services/groups"; -import {mapToUsername} from "../../../interfaces/user"; +import { authorize } from './auth-checks'; +import { fetchClass } from '../../../services/classes'; +import { fetchGroup } from '../../../services/groups'; +import { mapToUsername } from '../../../interfaces/user'; /** * Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'. @@ -9,17 +9,17 @@ import {mapToUsername} from "../../../interfaces/user"; * - either teachers of the class the assignment for the group was posted in, * - or students in the group */ -export const onlyAllowIfHasAccessToGroup = authorize( - async (auth, req) => { - const { classid: classId, assignmentid: assignmentId, groupid: groupId } = - req.params as { classid: string, assignmentid: number, groupid: number }; +export const onlyAllowIfHasAccessToGroup = authorize(async (auth, req) => { + const { + classid: classId, + assignmentid: assignmentId, + groupid: groupId, + } = req.params as { classid: string; assignmentid: number; groupid: number }; - if (auth.accountType === "teacher") { - const clazz = await fetchClass(classId); - return clazz.teachers.map(mapToUsername).includes(auth.username); - } // User is student - const group = await fetchGroup(classId, assignmentId, groupId); - return group.members.map(mapToUsername).includes(auth.username); - - } -); + if (auth.accountType === 'teacher') { + const clazz = await fetchClass(classId); + return clazz.teachers.map(mapToUsername).includes(auth.username); + } // User is student + const group = await fetchGroup(classId, assignmentId, groupId); + return group.members.map(mapToUsername).includes(auth.username); +}); diff --git a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts index a13cd038..c0b34c52 100644 --- a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts +++ b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts @@ -1,6 +1,6 @@ -import {authorize} from "./auth-checks"; -import {AuthenticationInfo} from "../authentication-info"; -import {AuthenticatedRequest} from "../authenticated-request"; +import { authorize } from './auth-checks'; +import { AuthenticationInfo } from '../authentication-info'; +import { AuthenticatedRequest } from '../authenticated-request'; /** * Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId') @@ -9,15 +9,12 @@ import {AuthenticatedRequest} from "../authenticated-request"; * - or set to a group the user is in, * - or set to anything if the user is a teacher. */ -export const onlyAllowPersonalizationForOwnGroup = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const {forGroup, assignmentNo, classId} = req.params; - if (auth.accountType === "student" && forGroup && assignmentNo && classId) { - // TODO: groupNumber? - // Const group = await fetchGroup(Number(classId), Number(assignmentNo), ) - return false; - } - return true; - +export const onlyAllowPersonalizationForOwnGroup = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const { forGroup, assignmentNo, classId } = req.params; + if (auth.accountType === 'student' && forGroup && assignmentNo && classId) { + // TODO: groupNumber? + // Const group = await fetchGroup(Number(classId), Number(assignmentNo), ) + return false; } -); + return true; +}); diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts index 38b1f0ef..c0a4329f 100644 --- a/backend/src/middleware/auth/checks/question-checks.ts +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -1,72 +1,65 @@ -import {authorize} from "./auth-checks"; -import {AuthenticationInfo} from "../authentication-info"; -import {AuthenticatedRequest} from "../authenticated-request"; -import {requireFields} from "../../../controllers/error-helper"; -import {getLearningObjectId, getQuestionId} from "../../../controllers/questions"; -import {fetchQuestion} from "../../../services/questions"; -import {FALLBACK_SEQ_NUM} from "../../../config"; -import {fetchAnswer} from "../../../services/answers"; -import {mapToUsername} from "../../../interfaces/user"; +import { authorize } from './auth-checks'; +import { AuthenticationInfo } from '../authentication-info'; +import { AuthenticatedRequest } from '../authenticated-request'; +import { requireFields } from '../../../controllers/error-helper'; +import { getLearningObjectId, getQuestionId } from '../../../controllers/questions'; +import { fetchQuestion } from '../../../services/questions'; +import { FALLBACK_SEQ_NUM } from '../../../config'; +import { fetchAnswer } from '../../../services/answers'; +import { mapToUsername } from '../../../interfaces/user'; export const onlyAllowAuthor = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username ); -export const onlyAllowAuthorRequest = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const hruid = req.params.hruid; - const version = req.params.version; - const language = req.query.lang as string; - const seq = req.params.seq; - requireFields({ hruid }); +export const onlyAllowAuthorRequest = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }); - const learningObjectId = getLearningObjectId(hruid, version, language); - const questionId = getQuestionId(learningObjectId, seq); + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); - const question = await fetchQuestion(questionId); + const question = await fetchQuestion(questionId); - return question.author.username === auth.username; - } -); + return question.author.username === auth.username; +}); -export const onlyAllowAuthorRequestAnswer = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const hruid = req.params.hruid; - const version = req.params.version; - const language = req.query.lang as string; - const seq = req.params.seq; - const seqAnswer = req.params.seqAnswer; - requireFields({ hruid }); +export const onlyAllowAuthorRequestAnswer = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + const seqAnswer = req.params.seqAnswer; + requireFields({ hruid }); - const learningObjectId = getLearningObjectId(hruid, version, language); - const questionId = getQuestionId(learningObjectId, seq); + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); - const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; - const answer = await fetchAnswer(questionId, sequenceNumber); + const sequenceNumber = Number(seqAnswer) || FALLBACK_SEQ_NUM; + const answer = await fetchAnswer(questionId, sequenceNumber); - return answer.author.username === auth.username; - } -); + return answer.author.username === auth.username; +}); -export const onlyAllowIfHasAccessToQuestion = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const hruid = req.params.hruid; - const version = req.params.version; - const language = req.query.lang as string; - const seq = req.params.seq; - requireFields({ hruid }); +export const onlyAllowIfHasAccessToQuestion = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const hruid = req.params.hruid; + const version = req.params.version; + const language = req.query.lang as string; + const seq = req.params.seq; + requireFields({ hruid }); - const learningObjectId = getLearningObjectId(hruid, version, language); - const questionId = getQuestionId(learningObjectId, seq); + const learningObjectId = getLearningObjectId(hruid, version, language); + const questionId = getQuestionId(learningObjectId, seq); - const question = await fetchQuestion(questionId); - const group = question.inGroup; + const question = await fetchQuestion(questionId); + const group = question.inGroup; - if (auth.accountType === "teacher") { - const cls = group.assignment.within; // TODO check if contains full objects - return cls.teachers.map(mapToUsername).includes(auth.username); - } // User is student - return group.members.map(mapToUsername).includes(auth.username); - - } -); + if (auth.accountType === 'teacher') { + const cls = group.assignment.within; // TODO check if contains full objects + return cls.teachers.map(mapToUsername).includes(auth.username); + } // User is student + return group.members.map(mapToUsername).includes(auth.username); +}); diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index 78087fa9..87171584 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -1,29 +1,27 @@ -import { languageMap } from "dwengo-1-common/util/language"; -import { LearningObjectIdentifier } from "../../../entities/content/learning-object-identifier"; -import { fetchSubmission } from "../../../services/submissions"; -import { AuthenticatedRequest } from "../authenticated-request"; -import { AuthenticationInfo } from "../authentication-info"; -import { authorize } from "./auth-checks"; -import { FALLBACK_LANG } from "../../../config"; -import { mapToUsername } from "../../../interfaces/user"; +import { languageMap } from 'dwengo-1-common/util/language'; +import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier'; +import { fetchSubmission } from '../../../services/submissions'; +import { AuthenticatedRequest } from '../authenticated-request'; +import { AuthenticationInfo } from '../authentication-info'; +import { authorize } from './auth-checks'; +import { FALLBACK_LANG } from '../../../config'; +import { mapToUsername } from '../../../interfaces/user'; export const onlyAllowSubmitter = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username ); -export const onlyAllowIfHasAccessToSubmission = authorize( - async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const { hruid: lohruid, id: submissionNumber } = req.params; - const { language: lang, version: version } = req.query; +export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const { hruid: lohruid, id: submissionNumber } = req.params; + const { language: lang, version: version } = req.query; - const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version)) - const submission = await fetchSubmission(loId, Number(submissionNumber)); + const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version)); + const submission = await fetchSubmission(loId, Number(submissionNumber)); - if (auth.accountType === "teacher") { - // Dit kan niet werken om dat al deze objecten niet gepopulate zijn. - return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username); - } - - return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username); + if (auth.accountType === 'teacher') { + // Dit kan niet werken om dat al deze objecten niet gepopulate zijn. + return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username); } -) \ No newline at end of file + + return submission.onBehalfOf.members.map(mapToUsername).includes(auth.username); +}); diff --git a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts index 27d0cab2..5f38eaf1 100644 --- a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts +++ b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts @@ -1,23 +1,17 @@ -import {authorize} from "./auth-checks"; -import {AuthenticationInfo} from "../authentication-info"; -import {AuthenticatedRequest} from "../authenticated-request"; +import { authorize } from './auth-checks'; +import { AuthenticationInfo } from '../authentication-info'; +import { AuthenticatedRequest } from '../authenticated-request'; export const onlyAllowSenderOrReceiver = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => - req.params.sender === auth.username || req.params.receiver === auth.username + (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username || req.params.receiver === auth.username ); -export const onlyAllowSender = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => - req.params.sender === auth.username -); +export const onlyAllowSender = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username); export const onlyAllowSenderBody = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => - (req.body as { sender: string }).sender === auth.username + (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { sender: string }).sender === auth.username ); export const onlyAllowReceiverBody = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => - (req.body as { receiver: string }).receiver === auth.username + (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { receiver: string }).receiver === auth.username ); diff --git a/backend/src/middleware/auth/checks/user-auth-checks.ts b/backend/src/middleware/auth/checks/user-auth-checks.ts index 10814eb8..bbe4c7f2 100644 --- a/backend/src/middleware/auth/checks/user-auth-checks.ts +++ b/backend/src/middleware/auth/checks/user-auth-checks.ts @@ -1,10 +1,8 @@ -import {authorize} from "./auth-checks"; -import {AuthenticationInfo} from "../authentication-info"; -import {AuthenticatedRequest} from "../authenticated-request"; +import { authorize } from './auth-checks'; +import { AuthenticationInfo } from '../authentication-info'; +import { AuthenticatedRequest } from '../authenticated-request'; /** * Only allow the user whose username is in the path parameter "username" to access the endpoint. */ -export const onlyAllowUserHimself = authorize( - (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username -); +export const onlyAllowUserHimself = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username); diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index a5ee6278..6e76a543 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,12 +1,7 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; -import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; -import { - onlyAllowAuthor, - onlyAllowAuthorRequestAnswer, - onlyAllowIfHasAccessToQuestion -} from "../middleware/auth/checks/question-checks"; - +import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks'; +import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index 7b2d66fa..2a38748b 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -8,9 +8,9 @@ import { putAssignmentHandler, } from '../controllers/assignments.js'; import groupRouter from './groups.js'; -import {teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowIfInClass} from "../middleware/auth/checks/class-auth-checks"; -import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks"; +import { teachersOnly } from '../middleware/auth/checks/auth-checks'; +import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks'; +import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index 9cf20ec0..9889151d 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -14,8 +14,8 @@ import { putClassHandler, } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; -import {adminOnly, teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowIfInClass} from "../middleware/auth/checks/class-auth-checks"; +import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks'; +import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks'; const router = express.Router(); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 5d3d8ed0..cb92e7c2 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -7,9 +7,9 @@ import { getGroupSubmissionsHandler, putGroupHandler, } from '../controllers/groups.js'; -import {onlyAllowIfHasAccessToGroup} from "../middleware/auth/checks/group-auth-checker"; -import {teachersOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowIfHasAccessToAssignment} from "../middleware/auth/checks/assignment-auth-checks"; +import { onlyAllowIfHasAccessToGroup } from '../middleware/auth/checks/group-auth-checker'; +import { teachersOnly } from '../middleware/auth/checks/auth-checks'; +import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index fb65d9cd..fa438a57 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -3,7 +3,7 @@ import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObj import submissionRoutes from './submissions.js'; import questionRoutes from './questions.js'; -import {authenticatedOnly} from "../middleware/auth/checks/auth-checks"; +import { authenticatedOnly } from '../middleware/auth/checks/auth-checks'; const router = express.Router(); diff --git a/backend/src/routes/learning-paths.ts b/backend/src/routes/learning-paths.ts index ad079551..ee3c4955 100644 --- a/backend/src/routes/learning-paths.ts +++ b/backend/src/routes/learning-paths.ts @@ -1,6 +1,6 @@ import express from 'express'; import { getLearningPaths } from '../controllers/learning-paths.js'; -import {authenticatedOnly} from "../middleware/auth/checks/auth-checks"; +import { authenticatedOnly } from '../middleware/auth/checks/auth-checks'; const router = express.Router(); diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index bffcbe9e..a15c1d0a 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,13 +1,9 @@ import express from 'express'; import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; -import {adminOnly, studentsOnly} from "../middleware/auth/checks/auth-checks"; -import {updateAnswerHandler} from "../controllers/answers"; -import { - onlyAllowAuthor, - onlyAllowAuthorRequest, - onlyAllowIfHasAccessToQuestion -} from "../middleware/auth/checks/question-checks"; +import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks'; +import { updateAnswerHandler } from '../controllers/answers'; +import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks'; const router = express.Router({ mergeParams: true }); @@ -23,7 +19,7 @@ router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler); router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler); -router.put("/:seq", studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler); +router.put('/:seq', studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler); router.use('/:seq/answers', answerRoutes); diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts index 66a6c75e..f467f346 100644 --- a/backend/src/routes/student-join-requests.ts +++ b/backend/src/routes/student-join-requests.ts @@ -5,8 +5,8 @@ import { getStudentRequestHandler, getStudentRequestsHandler, } from '../controllers/students.js'; -import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; -import {onlyAllowStudentHimselfAndTeachersOfClass} from "../middleware/auth/checks/class-auth-checks"; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; +import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks'; // Under /:username/joinRequests/ diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index 04d260b4..e0634219 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -11,8 +11,8 @@ import { getStudentSubmissionsHandler, } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; -import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; -import {adminOnly} from "../middleware/auth/checks/auth-checks"; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; +import { adminOnly } from '../middleware/auth/checks/auth-checks'; const router = express.Router(); diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index fe0b924f..d9c98e5f 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -6,18 +6,19 @@ import { getInvitationHandler, updateInvitationHandler, } from '../controllers/teacher-invitations'; -import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; import { - onlyAllowReceiverBody, onlyAllowSender, + onlyAllowReceiverBody, + onlyAllowSender, onlyAllowSenderBody, - onlyAllowSenderOrReceiver -} from "../middleware/auth/checks/teacher-invitation-checks"; + onlyAllowSenderOrReceiver, +} from '../middleware/auth/checks/teacher-invitation-checks'; const router = express.Router({ mergeParams: true }); router.get('/:username', onlyAllowUserHimself, getAllInvitationsHandler); -router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver ,getInvitationHandler); +router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver, getInvitationHandler); router.post('/', onlyAllowSenderBody, createInvitationHandler); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index 776b9951..edd06118 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -12,9 +12,9 @@ import { } from '../controllers/teachers.js'; import invitationRouter from './teacher-invitations.js'; -import {adminOnly} from "../middleware/auth/checks/auth-checks"; -import {onlyAllowUserHimself} from "../middleware/auth/checks/user-auth-checks"; -import {onlyAllowTeacherOfClass} from "../middleware/auth/checks/class-auth-checks"; +import { adminOnly } from '../middleware/auth/checks/auth-checks'; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; +import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks'; const router = express.Router(); // Root endpoint used to search objects diff --git a/backend/src/routes/themes.ts b/backend/src/routes/themes.ts index 089c5d46..c5882a2a 100644 --- a/backend/src/routes/themes.ts +++ b/backend/src/routes/themes.ts @@ -1,6 +1,6 @@ import express from 'express'; import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js'; -import {authenticatedOnly} from "../middleware/auth/checks/auth-checks"; +import { authenticatedOnly } from '../middleware/auth/checks/auth-checks'; const router = express.Router(); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 70400226..5894ad6f 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -12,7 +12,7 @@ import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { fetchStudent } from './students.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { FALLBACK_VERSION_NUM } from '../config.js'; -import {ConflictException} from "../exceptions/conflict-exception"; +import { ConflictException } from '../exceptions/conflict-exception'; export async function getQuestionsAboutLearningObjectInAssignment( loId: LearningObjectIdentifier, @@ -91,12 +91,12 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat const assignment = mapToAssignment(questionData.inGroup.assignment as AssignmentDTO, clazz!); const group = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber); - if (!group){ - throw new NotFoundException("Group with id and assignment not found"); + if (!group) { + throw new NotFoundException('Group with id and assignment not found'); } - if (! group.members.contains(author)) { - throw new ConflictException("Author is not part of this group"); + if (!group.members.contains(author)) { + throw new ConflictException('Author is not part of this group'); } const question = await questionRepository.createQuestion({ diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 09ba1643..de6a9254 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -25,7 +25,7 @@ import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; import { Submission } from '../entities/assignments/submission.entity'; -import {mapToUsername} from "../interfaces/user"; +import { mapToUsername } from '../interfaces/user'; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); diff --git a/backend/src/services/teacher-invitations.ts b/backend/src/services/teacher-invitations.ts index 6aec819b..0457496f 100644 --- a/backend/src/services/teacher-invitations.ts +++ b/backend/src/services/teacher-invitations.ts @@ -32,7 +32,7 @@ export async function createInvitation(data: TeacherInvitationData): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); From 6c1aeb2331a2e746e4116214384cfa4e2051dd06 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Thu, 24 Apr 2025 10:44:54 +0200 Subject: [PATCH 018/117] fix: error import --- backend/src/middleware/auth/checks/submission-checks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index 87171584..b4e7b1a5 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -1,4 +1,3 @@ -import { languageMap } from 'dwengo-1-common/util/language'; import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier'; import { fetchSubmission } from '../../../services/submissions'; import { AuthenticatedRequest } from '../authenticated-request'; @@ -6,6 +5,7 @@ import { AuthenticationInfo } from '../authentication-info'; import { authorize } from './auth-checks'; import { FALLBACK_LANG } from '../../../config'; import { mapToUsername } from '../../../interfaces/user'; +import { languageMap } from "@dwengo-1/common/util/language"; export const onlyAllowSubmitter = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username From 68479616889cfa28a456cd32c332fb9f36025d66 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Thu, 24 Apr 2025 08:47:39 +0000 Subject: [PATCH 019/117] style: fix linting issues met Prettier --- backend/src/middleware/auth/checks/submission-checks.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index b4e7b1a5..f7a33a3d 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -5,7 +5,7 @@ import { AuthenticationInfo } from '../authentication-info'; import { authorize } from './auth-checks'; import { FALLBACK_LANG } from '../../../config'; import { mapToUsername } from '../../../interfaces/user'; -import { languageMap } from "@dwengo-1/common/util/language"; +import { languageMap } from '@dwengo-1/common/util/language'; export const onlyAllowSubmitter = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username From 04fd54e3d623642b880a5ab0b9017f1a7825cf40 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Thu, 24 Apr 2025 10:53:08 +0200 Subject: [PATCH 020/117] fix: .js toevoegen aan imports --- backend/src/controllers/auth.ts | 6 +++--- backend/src/controllers/teacher-invitations.ts | 2 +- .../auth/checks/assignment-auth-checks.ts | 8 ++++---- .../src/middleware/auth/checks/auth-checks.ts | 8 ++++---- .../auth/checks/class-auth-checks.ts | 10 +++++----- .../auth/checks/group-auth-checker.ts | 8 ++++---- .../middleware/auth/checks/question-checks.ts | 18 +++++++++--------- .../auth/checks/submission-checks.ts | 14 +++++++------- .../auth/checks/teacher-invitation-checks.ts | 6 +++--- .../middleware/auth/checks/user-auth-checks.ts | 6 +++--- backend/src/routes/answers.ts | 4 ++-- backend/src/routes/assignments.ts | 6 +++--- backend/src/routes/classes.ts | 4 ++-- backend/src/routes/groups.ts | 6 +++--- backend/src/routes/learning-objects.ts | 3 +-- backend/src/routes/learning-paths.ts | 2 +- backend/src/routes/questions.ts | 6 +++--- backend/src/routes/student-join-requests.ts | 4 ++-- backend/src/routes/students.ts | 4 ++-- backend/src/routes/teacher-invitations.ts | 6 +++--- backend/src/routes/teachers.ts | 7 +++---- backend/src/routes/themes.ts | 2 +- backend/src/services/questions.ts | 2 +- backend/src/services/students.ts | 4 ++-- backend/src/services/teachers.ts | 2 +- 25 files changed, 73 insertions(+), 75 deletions(-) diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index 6395715d..d8eaade5 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -2,10 +2,10 @@ import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; import { getLogger } from '../logging/initalize.js'; import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; import { envVars, getEnvVar } from '../util/envVars.js'; -import { createOrUpdateStudent, createStudent } from '../services/students'; -import { AuthenticationInfo } from '../middleware/auth/authentication-info'; +import { createOrUpdateStudent, createStudent } from '../services/students.js'; +import { AuthenticationInfo } from '../middleware/auth/authentication-info.js'; import { Request, Response } from 'express'; -import { createOrUpdateTeacher, createTeacher } from '../services/teachers'; +import { createOrUpdateTeacher, createTeacher } from '../services/teachers.js'; interface FrontendIdpConfig { authority: string; diff --git a/backend/src/controllers/teacher-invitations.ts b/backend/src/controllers/teacher-invitations.ts index f1350660..9e8eee6e 100644 --- a/backend/src/controllers/teacher-invitations.ts +++ b/backend/src/controllers/teacher-invitations.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { requireFields } from './error-helper.js'; import { createInvitation, deleteInvitation, getAllInvitations, getInvitation, updateInvitation } from '../services/teacher-invitations.js'; import { TeacherInvitationData } from '@dwengo-1/common/interfaces/teacher-invitation'; -import { ConflictException } from '../exceptions/conflict-exception'; +import { ConflictException } from '../exceptions/conflict-exception.js'; export async function getAllInvitationsHandler(req: Request, res: Response): Promise { const username = req.params.username; diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts index 8328dec0..f8e1e3d7 100644 --- a/backend/src/middleware/auth/checks/assignment-auth-checks.ts +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -1,7 +1,7 @@ -import { authorize } from './auth-checks'; -import { fetchClass } from '../../../services/classes'; -import { fetchAllGroups } from '../../../services/groups'; -import { mapToUsername } from '../../../interfaces/user'; +import { authorize } from './auth-checks.js'; +import { fetchClass } from '../../../services/classes.js'; +import { fetchAllGroups } from '../../../services/groups.js'; +import { mapToUsername } from '../../../interfaces/user.js'; /** * Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment). diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts index a4a75db5..6afe92e7 100644 --- a/backend/src/middleware/auth/checks/auth-checks.ts +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -1,9 +1,9 @@ -import { AuthenticationInfo } from '../authentication-info'; -import { AuthenticatedRequest } from '../authenticated-request'; +import { AuthenticationInfo } from '../authentication-info.js'; +import { AuthenticatedRequest } from '../authenticated-request.js'; import * as express from 'express'; -import { UnauthorizedException } from '../../../exceptions/unauthorized-exception'; -import { ForbiddenException } from '../../../exceptions/forbidden-exception'; import { RequestHandler } from 'express'; +import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js'; +import { ForbiddenException } from '../../../exceptions/forbidden-exception.js'; /** * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 6aae97b3..7093b0d1 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -1,8 +1,8 @@ -import { authorize } from './auth-checks'; -import { AuthenticationInfo } from '../authentication-info'; -import { AuthenticatedRequest } from '../authenticated-request'; -import { fetchClass } from '../../../services/classes'; -import { mapToUsername } from '../../../interfaces/user'; +import { authorize } from './auth-checks.js'; +import { AuthenticationInfo } from '../authentication-info.js'; +import { AuthenticatedRequest } from '../authenticated-request.js'; +import { fetchClass } from '../../../services/classes.js'; +import { mapToUsername } from '../../../interfaces/user.js'; async function teaches(teacherUsername: string, classId: string): Promise { const clazz = await fetchClass(classId); diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts index bebda954..02230b13 100644 --- a/backend/src/middleware/auth/checks/group-auth-checker.ts +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -1,7 +1,7 @@ -import { authorize } from './auth-checks'; -import { fetchClass } from '../../../services/classes'; -import { fetchGroup } from '../../../services/groups'; -import { mapToUsername } from '../../../interfaces/user'; +import { authorize } from './auth-checks.js'; +import { fetchClass } from '../../../services/classes.js'; +import { fetchGroup } from '../../../services/groups.js'; +import { mapToUsername } from '../../../interfaces/user.js'; /** * Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'. diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts index c0a4329f..374a2aa0 100644 --- a/backend/src/middleware/auth/checks/question-checks.ts +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -1,12 +1,12 @@ -import { authorize } from './auth-checks'; -import { AuthenticationInfo } from '../authentication-info'; -import { AuthenticatedRequest } from '../authenticated-request'; -import { requireFields } from '../../../controllers/error-helper'; -import { getLearningObjectId, getQuestionId } from '../../../controllers/questions'; -import { fetchQuestion } from '../../../services/questions'; -import { FALLBACK_SEQ_NUM } from '../../../config'; -import { fetchAnswer } from '../../../services/answers'; -import { mapToUsername } from '../../../interfaces/user'; +import { authorize } from './auth-checks.js'; +import { AuthenticationInfo } from '../authentication-info.js'; +import { AuthenticatedRequest } from '../authenticated-request.js'; +import { requireFields } from '../../../controllers/error-helper.js'; +import { getLearningObjectId, getQuestionId } from '../../../controllers/questions.js'; +import { fetchQuestion } from '../../../services/questions.js'; +import { FALLBACK_SEQ_NUM } from '../../../config.js'; +import { fetchAnswer } from '../../../services/answers.js'; +import { mapToUsername } from '../../../interfaces/user.js'; export const onlyAllowAuthor = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index f7a33a3d..cb84f438 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -1,11 +1,11 @@ -import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier'; -import { fetchSubmission } from '../../../services/submissions'; -import { AuthenticatedRequest } from '../authenticated-request'; -import { AuthenticationInfo } from '../authentication-info'; -import { authorize } from './auth-checks'; -import { FALLBACK_LANG } from '../../../config'; -import { mapToUsername } from '../../../interfaces/user'; import { languageMap } from '@dwengo-1/common/util/language'; +import { LearningObjectIdentifier } from '../../../entities/content/learning-object-identifier.js'; +import { fetchSubmission } from '../../../services/submissions.js'; +import { AuthenticatedRequest } from '../authenticated-request.js'; +import { AuthenticationInfo } from '../authentication-info.js'; +import { authorize } from './auth-checks.js'; +import { FALLBACK_LANG } from '../../../config.js'; +import { mapToUsername } from '../../../interfaces/user.js'; export const onlyAllowSubmitter = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username diff --git a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts index 5f38eaf1..0c6a790f 100644 --- a/backend/src/middleware/auth/checks/teacher-invitation-checks.ts +++ b/backend/src/middleware/auth/checks/teacher-invitation-checks.ts @@ -1,6 +1,6 @@ -import { authorize } from './auth-checks'; -import { AuthenticationInfo } from '../authentication-info'; -import { AuthenticatedRequest } from '../authenticated-request'; +import { authorize } from './auth-checks.js'; +import { AuthenticationInfo } from '../authentication-info.js'; +import { AuthenticatedRequest } from '../authenticated-request.js'; export const onlyAllowSenderOrReceiver = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.sender === auth.username || req.params.receiver === auth.username diff --git a/backend/src/middleware/auth/checks/user-auth-checks.ts b/backend/src/middleware/auth/checks/user-auth-checks.ts index bbe4c7f2..f66f6682 100644 --- a/backend/src/middleware/auth/checks/user-auth-checks.ts +++ b/backend/src/middleware/auth/checks/user-auth-checks.ts @@ -1,6 +1,6 @@ -import { authorize } from './auth-checks'; -import { AuthenticationInfo } from '../authentication-info'; -import { AuthenticatedRequest } from '../authenticated-request'; +import { authorize } from './auth-checks.js'; +import { AuthenticationInfo } from '../authentication-info.js'; +import { AuthenticatedRequest } from '../authenticated-request.js'; /** * Only allow the user whose username is in the path parameter "username" to access the endpoint. diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index 6e76a543..e0cf5b17 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,7 +1,7 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; -import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks'; -import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks'; +import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; +import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index cb2ff268..f0250550 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -9,9 +9,9 @@ import { putAssignmentHandler, } from '../controllers/assignments.js'; import groupRouter from './groups.js'; -import { teachersOnly } from '../middleware/auth/checks/auth-checks'; -import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks'; -import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks'; +import { teachersOnly } from '../middleware/auth/checks/auth-checks.js'; +import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks.js'; +import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index 9889151d..4b272971 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -14,8 +14,8 @@ import { putClassHandler, } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; -import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks'; -import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks'; +import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; +import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks.js'; const router = express.Router(); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index bcde8f45..086c5eff 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -8,9 +8,9 @@ import { getGroupSubmissionsHandler, putGroupHandler, } from '../controllers/groups.js'; -import { onlyAllowIfHasAccessToGroup } from '../middleware/auth/checks/group-auth-checker'; -import { teachersOnly } from '../middleware/auth/checks/auth-checks'; -import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks'; +import { onlyAllowIfHasAccessToGroup } from '../middleware/auth/checks/group-auth-checker.js'; +import { teachersOnly } from '../middleware/auth/checks/auth-checks.js'; +import { onlyAllowIfHasAccessToAssignment } from '../middleware/auth/checks/assignment-auth-checks.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index fa438a57..f53f208a 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,9 +1,8 @@ import express from 'express'; import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; - import submissionRoutes from './submissions.js'; import questionRoutes from './questions.js'; -import { authenticatedOnly } from '../middleware/auth/checks/auth-checks'; +import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router(); diff --git a/backend/src/routes/learning-paths.ts b/backend/src/routes/learning-paths.ts index ee3c4955..59b85e62 100644 --- a/backend/src/routes/learning-paths.ts +++ b/backend/src/routes/learning-paths.ts @@ -1,6 +1,6 @@ import express from 'express'; import { getLearningPaths } from '../controllers/learning-paths.js'; -import { authenticatedOnly } from '../middleware/auth/checks/auth-checks'; +import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router(); diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index a15c1d0a..76ec4eaa 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,9 +1,9 @@ import express from 'express'; import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; -import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks'; -import { updateAnswerHandler } from '../controllers/answers'; -import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks'; +import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; +import { updateAnswerHandler } from '../controllers/answers.js'; +import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts index f467f346..35198d0c 100644 --- a/backend/src/routes/student-join-requests.ts +++ b/backend/src/routes/student-join-requests.ts @@ -5,8 +5,8 @@ import { getStudentRequestHandler, getStudentRequestsHandler, } from '../controllers/students.js'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; -import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks'; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks.js'; // Under /:username/joinRequests/ diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index e0634219..f40ce939 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -11,8 +11,8 @@ import { getStudentSubmissionsHandler, } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; -import { adminOnly } from '../middleware/auth/checks/auth-checks'; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { adminOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router(); diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index d9c98e5f..0855c6a6 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -5,14 +5,14 @@ import { getAllInvitationsHandler, getInvitationHandler, updateInvitationHandler, -} from '../controllers/teacher-invitations'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; +} from '../controllers/teacher-invitations.js'; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; import { onlyAllowReceiverBody, onlyAllowSender, onlyAllowSenderBody, onlyAllowSenderOrReceiver, -} from '../middleware/auth/checks/teacher-invitation-checks'; +} from '../middleware/auth/checks/teacher-invitation-checks.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index edd06118..26ec77be 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -11,10 +11,9 @@ import { updateStudentJoinRequestHandler, } from '../controllers/teachers.js'; import invitationRouter from './teacher-invitations.js'; - -import { adminOnly } from '../middleware/auth/checks/auth-checks'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks'; -import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks'; +import { adminOnly } from '../middleware/auth/checks/auth-checks.js'; +import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks.js'; const router = express.Router(); // Root endpoint used to search objects diff --git a/backend/src/routes/themes.ts b/backend/src/routes/themes.ts index c5882a2a..6310c2ab 100644 --- a/backend/src/routes/themes.ts +++ b/backend/src/routes/themes.ts @@ -1,6 +1,6 @@ import express from 'express'; import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js'; -import { authenticatedOnly } from '../middleware/auth/checks/auth-checks'; +import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router(); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 5894ad6f..8e2d245d 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -12,7 +12,7 @@ import { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment'; import { fetchStudent } from './students.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { FALLBACK_VERSION_NUM } from '../config.js'; -import { ConflictException } from '../exceptions/conflict-exception'; +import { ConflictException } from '../exceptions/conflict-exception.js'; export async function getQuestionsAboutLearningObjectInAssignment( loId: LearningObjectIdentifier, diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index de6a9254..2a003d3a 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -24,8 +24,8 @@ import { SubmissionDTO, SubmissionDTOId } from '@dwengo-1/common/interfaces/subm import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; -import { Submission } from '../entities/assignments/submission.entity'; -import { mapToUsername } from '../interfaces/user'; +import { Submission } from '../entities/assignments/submission.entity.js'; +import { mapToUsername } from '../interfaces/user.js'; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 46336bd7..aa67f211 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -30,7 +30,7 @@ import { QuestionDTO, QuestionId } from '@dwengo-1/common/interfaces/question'; import { ClassJoinRequestDTO } from '@dwengo-1/common/interfaces/class-join-request'; import { ClassStatus } from '@dwengo-1/common/util/class-join-request'; import { ConflictException } from '../exceptions/conflict-exception.js'; -import { mapToUsername } from '../interfaces/user'; +import { mapToUsername } from '../interfaces/user.js'; export async function getAllTeachers(full: boolean): Promise { const teacherRepository: TeacherRepository = getTeacherRepository(); From 0b9f7653660529c32b00227301c5931613531ed6 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 29 Apr 2025 23:06:58 +0200 Subject: [PATCH 021/117] fix(backend): 409 Conflict bij het oproepen van /hello met een reeds geregistreerd account. --- backend/src/controllers/auth.ts | 30 ++---------------------------- backend/src/routes/auth.ts | 10 ++++------ 2 files changed, 6 insertions(+), 34 deletions(-) diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index d8eaade5..ca79da59 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -2,10 +2,9 @@ import { UnauthorizedException } from '../exceptions/unauthorized-exception.js'; import { getLogger } from '../logging/initalize.js'; import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; import { envVars, getEnvVar } from '../util/envVars.js'; -import { createOrUpdateStudent, createStudent } from '../services/students.js'; -import { AuthenticationInfo } from '../middleware/auth/authentication-info.js'; +import { createOrUpdateStudent } from '../services/students.js'; import { Request, Response } from 'express'; -import { createOrUpdateTeacher, createTeacher } from '../services/teachers.js'; +import { createOrUpdateTeacher } from '../services/teachers.js'; interface FrontendIdpConfig { authority: string; @@ -45,31 +44,6 @@ export function handleGetFrontendAuthConfig(_req: Request, res: Response): void res.json(getFrontendAuthConfig()); } -export async function handleHello(req: AuthenticatedRequest): Promise { - const auth: AuthenticationInfo = req.auth!; - if (auth.accountType === 'teacher') { - await createTeacher( - { - id: auth.username, - username: auth.username, - firstName: auth.firstName ?? '', - lastName: auth.lastName ?? '', - }, - true - ); - } else { - await createStudent( - { - id: auth.username, - username: auth.username, - firstName: auth.firstName ?? '', - lastName: auth.lastName ?? '', - }, - true - ); - } -} - export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise { const auth = req.auth; if (!auth) { diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index a7bd5497..b71b418d 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -1,15 +1,11 @@ import express from 'express'; -import { handleGetFrontendAuthConfig, handleHello, postHelloHandler } from '../controllers/auth.js'; +import { handleGetFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js'; import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router(); // Returns auth configuration for frontend -router.get('/config', handleGetFrontendAuthConfig); - -// This endpoint is called by the client when the user has just logged in. -// It creates or updates the user entity based on the authentication data the endpoint was called with. -router.post('/hello', authenticatedOnly, handleHello); +router.get('/config', handleGetFrontendAuthConfig) router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ @@ -26,6 +22,8 @@ router.get('/testTeachersOnly', teachersOnly, (_req, res) => { res.json({ message: 'If you see this, you should be a teacher!' }); }); +// This endpoint is called by the client when the user has just logged in. +// It creates or updates the user entity based on the authentication data the endpoint was called with. router.post('/hello', authenticatedOnly, postHelloHandler); export default router; From dee14791eae3e4381bbef61d1220e1534b488339 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 3 May 2025 13:43:23 +0200 Subject: [PATCH 022/117] test(frontend): Teacher can create class --- frontend/e2e/class.spec.ts | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) create mode 100644 frontend/e2e/class.spec.ts diff --git a/frontend/e2e/class.spec.ts b/frontend/e2e/class.spec.ts new file mode 100644 index 00000000..394e4b27 --- /dev/null +++ b/frontend/e2e/class.spec.ts @@ -0,0 +1,27 @@ +import { test, expect } from "@playwright/test" + +test("Teacher create a class", async ({ page }) => { + await page.goto("/") + + // Login + await page.getByRole("link", { name: "log in" }).click(); + await page.getByRole("button", { name: "teacher" }).click(); + await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1"); + await page.getByRole("textbox", { name: "Password" }).fill("password"); + await page.getByRole("button", { name: "Sign In" }).click(); + + // Go to class + await expect(page.getByRole('banner').getByRole('link', { name: 'Classes' })).toBeVisible(); + await page.getByRole('banner').getByRole('link', { name: 'Classes' }).click(); + + await expect(page.getByRole('heading', { name: 'Classes' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'classname classname' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'create' })).toBeVisible(); + + await page.getByRole('textbox', { name: 'classname classname' }).click(); + await page.getByRole('textbox', { name: 'classname classname' }).fill('DeTijdLoze'); + await page.getByRole('button', { name: 'create' }).click(); + + await expect(page.getByRole('dialog').getByText('code')).toBeVisible(); + await expect(page.getByRole('button', { name: 'close' })).toBeVisible(); +}); From d9e428f52215b3dfb644648045752c2554484c46 Mon Sep 17 00:00:00 2001 From: Tibo De Peuter Date: Sat, 3 May 2025 14:29:21 +0200 Subject: [PATCH 023/117] test(frontend): Student can join class by code --- frontend/e2e/class.spec.ts | 34 +++++++++++++++++++++++++++++++++- 1 file changed, 33 insertions(+), 1 deletion(-) diff --git a/frontend/e2e/class.spec.ts b/frontend/e2e/class.spec.ts index 394e4b27..f45f95b1 100644 --- a/frontend/e2e/class.spec.ts +++ b/frontend/e2e/class.spec.ts @@ -1,6 +1,8 @@ import { test, expect } from "@playwright/test" test("Teacher create a class", async ({ page }) => { + const className = "DeTijdLoze" + await page.goto("/") // Login @@ -14,14 +16,44 @@ test("Teacher create a class", async ({ page }) => { await expect(page.getByRole('banner').getByRole('link', { name: 'Classes' })).toBeVisible(); await page.getByRole('banner').getByRole('link', { name: 'Classes' }).click(); + // Check if the class page is visible await expect(page.getByRole('heading', { name: 'Classes' })).toBeVisible(); await expect(page.getByRole('textbox', { name: 'classname classname' })).toBeVisible(); await expect(page.getByRole('button', { name: 'create' })).toBeVisible(); + // Create a class await page.getByRole('textbox', { name: 'classname classname' }).click(); - await page.getByRole('textbox', { name: 'classname classname' }).fill('DeTijdLoze'); + await page.getByRole('textbox', { name: 'classname classname' }).fill(className); await page.getByRole('button', { name: 'create' }).click(); + // Check if the class is created await expect(page.getByRole('dialog').getByText('code')).toBeVisible(); await expect(page.getByRole('button', { name: 'close' })).toBeVisible(); }); + +test("Student can join class by code", async ({ page }) => { + await page.goto("/") + + // Login + await page.getByRole("link", { name: "log in" }).click(); + await page.getByRole("button", { name: "student" }).click(); + await page.getByRole("textbox", { name: "Username or email" }).fill("testleerling1"); + await page.getByRole("textbox", { name: "Password" }).fill("password"); + await page.getByRole("button", { name: "Sign In" }).click(); + + // Go to class + await expect(page.getByRole('banner').getByRole('link', { name: 'Classes' })).toBeVisible(); + await page.getByRole('banner').getByRole('link', { name: 'Classes' }).click(); + + // Check if the class page is visible + await expect(page.getByRole('heading', { name: 'Classes' })).toBeVisible(); + await expect(page.getByRole('heading', { name: 'Join class' })).toBeVisible(); + await expect(page.getByRole('textbox', { name: 'CODE CODE' })).toBeVisible(); + await expect(page.getByRole('button', { name: 'submit' })).toBeVisible(); + + // Join a class + await page.getByRole('textbox', { name: 'CODE CODE' }).click(); + await page.getByRole('textbox', { name: 'CODE CODE' }).fill('16c822ca-633d-49e3-89fc-8d7a291450e6'); + await page.getByRole('button', { name: 'submit' }).click(); + await expect(page.getByText('failed: Request failed with status code 404', { exact: true })).toBeVisible(); +}); From 86ba4ea11e5184b57b3da84324de32b18f2a9356 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 5 May 2025 23:15:22 +0200 Subject: [PATCH 024/117] feat(backend): Verwerking van leerobjecten in ZIP-formaat. --- backend/package.json | 4 + .../learning-object-service.ts | 23 +++- .../learning-object-zip-processing-service.ts | 63 +++++++++++ package-lock.json | 103 ++++++++++++++++++ 4 files changed, 192 insertions(+), 1 deletion(-) create mode 100644 backend/src/services/learning-objects/learning-object-zip-processing-service.ts diff --git a/backend/package.json b/backend/package.json index 7943d61d..7b2ab878 100644 --- a/backend/package.json +++ b/backend/package.json @@ -23,6 +23,8 @@ "@mikro-orm/postgresql": "6.4.12", "@mikro-orm/reflection": "6.4.12", "@mikro-orm/sqlite": "6.4.12", + "@types/mime-types": "^2.1.4", + "@types/unzipper": "^0.10.11", "axios": "^1.8.2", "cors": "^2.8.5", "cross": "^1.0.0", @@ -37,8 +39,10 @@ "jwks-rsa": "^3.1.0", "loki-logger-ts": "^1.0.2", "marked": "^15.0.7", + "mime-types": "^3.0.1", "response-time": "^2.3.3", "swagger-ui-express": "^5.0.1", + "unzipper": "^0.12.3", "uuid": "^11.1.0", "winston": "^3.17.0", "winston-loki": "^6.1.3" diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 7b4f47fc..4f5409fd 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -2,7 +2,13 @@ import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provid import { LearningObjectProvider } from './learning-object-provider.js'; import { envVars, getEnvVar } from '../../util/envVars.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js'; -import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { + FilteredLearningObject, + LearningObjectIdentifierDTO, + LearningPathIdentifier +} from '@dwengo-1/common/interfaces/learning-content'; +import {getLearningObjectRepository} from "../../data/repositories"; +import {processLearningObjectZip} from "./learning-object-zip-processing-service"; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -42,6 +48,21 @@ const learningObjectService = { async getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise { return getProvider(id).getLearningObjectHTML(id); }, + + + /** + * Store the learning object in the given zip file in the database. + */ + async storeLearningObject(learningObjectPath: string): Promise { + const learningObjectRepository = getLearningObjectRepository(); + const learningObject = await processLearningObjectZip(learningObjectPath); + + if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { + throw Error("Learning object name must start with the user content prefix!"); + } + + await learningObjectRepository.save(learningObject, {preventOverwrite: true}); + } }; export default learningObjectService; diff --git a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts new file mode 100644 index 00000000..3dbd5915 --- /dev/null +++ b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts @@ -0,0 +1,63 @@ +import unzipper from 'unzipper'; +import mime from 'mime-types'; +import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; +import {LearningObject} from "../../entities/content/learning-object.entity"; +import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; + +/** + * Process an uploaded zip file and construct a LearningObject from its contents. + * @param filePath Path of the zip file to process. + */ +export async function processLearningObjectZip(filePath: string): Promise { + const learningObjectRepo = getLearningObjectRepository(); + const attachmentRepo = getAttachmentRepository(); + + const zip = await unzipper.Open.file(filePath); + + let metadata: LearningObjectMetadata | null = null; + const attachments: {name: string, content: Buffer}[] = []; + let content: Buffer | null = null; + + for (const file of zip.files) { + if (file.type === "Directory") { + throw Error("The learning object zip file should not contain directories."); + } else if (file.path === "metadata.json") { + metadata = await processMetadataJson(file); + } else if (file.path.startsWith("index.")) { + content = await processFile(file); + } else { + attachments.push({ + name: file.path, + content: await processFile(file) + }); + } + } + + if (!metadata) { + throw Error("Missing metadata.json file"); + } + if (!content) { + throw Error("Missing index file"); + } + + const learningObject = learningObjectRepo.create(metadata); + const attachmentEntities = attachments.map(it => attachmentRepo.create({ + name: it.name, + content: it.content, + mimeType: mime.lookup(it.name) || "text/plain", + learningObject + })) + learningObject.attachments.push(...attachmentEntities); + + return learningObject; +} + +async function processMetadataJson(file: unzipper.File): LearningObjectMetadata { + const buf = await file.buffer(); + const content = buf.toString(); + return JSON.parse(content); +} + +async function processFile(file: unzipper.File): Promise { + return await file.buffer(); +} diff --git a/package-lock.json b/package-lock.json index d1a3b3a7..221d9fb5 100644 --- a/package-lock.json +++ b/package-lock.json @@ -36,6 +36,8 @@ "@mikro-orm/postgresql": "6.4.12", "@mikro-orm/reflection": "6.4.12", "@mikro-orm/sqlite": "6.4.12", + "@types/mime-types": "^2.1.4", + "@types/unzipper": "^0.10.11", "axios": "^1.8.2", "cors": "^2.8.5", "cross": "^1.0.0", @@ -50,8 +52,10 @@ "jwks-rsa": "^3.1.0", "loki-logger-ts": "^1.0.2", "marked": "^15.0.7", + "mime-types": "^3.0.1", "response-time": "^2.3.3", "swagger-ui-express": "^5.0.1", + "unzipper": "^0.12.3", "uuid": "^11.1.0", "winston": "^3.17.0", "winston-loki": "^6.1.3" @@ -1716,6 +1720,12 @@ "version": "1.3.5", "license": "MIT" }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "license": "MIT" + }, "node_modules/@types/ms": { "version": "2.1.0", "license": "MIT" @@ -1784,6 +1794,15 @@ "license": "MIT", "optional": true }, + "node_modules/@types/unzipper": { + "version": "0.10.11", + "resolved": "https://registry.npmjs.org/@types/unzipper/-/unzipper-0.10.11.tgz", + "integrity": "sha512-D25im2zjyMCcgL9ag6N46+wbtJBnXIr7SI4zHf9eJD2Dw2tEB5e+p5MYkrxKIVRscs5QV0EhtU9rgXSPx90oJg==", + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/web-bluetooth": { "version": "0.0.21", "license": "MIT" @@ -2711,6 +2730,12 @@ "readable-stream": "^3.4.0" } }, + "node_modules/bluebird": { + "version": "3.7.2", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz", + "integrity": "sha512-XpNj6GDQzdfW+r2Wnn7xiSAd7TM3jzkxGXBGTtWKuSXv1xUV+azxAm8jdWZN06QTQk+2N2XB9jRDkvbmQmcRtg==", + "license": "MIT" + }, "node_modules/body-parser": { "version": "2.2.0", "license": "MIT", @@ -3259,6 +3284,12 @@ "url": "https://github.com/sponsors/mesqueeb" } }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", + "license": "MIT" + }, "node_modules/cors": { "version": "2.8.5", "license": "MIT", @@ -3515,6 +3546,45 @@ "node": ">= 0.4" } }, + "node_modules/duplexer2": { + "version": "0.1.4", + "resolved": "https://registry.npmjs.org/duplexer2/-/duplexer2-0.1.4.tgz", + "integrity": "sha512-asLFVfWWtJ90ZyOUHMqk7/S2w2guQKxUI2itj3d92ADHhxUSbCMGi1f1cBcJ7xM1To+pE/Khbwo1yuNbMEPKeA==", + "license": "BSD-3-Clause", + "dependencies": { + "readable-stream": "^2.0.2" + } + }, + "node_modules/duplexer2/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/duplexer2/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "license": "MIT" + }, + "node_modules/duplexer2/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/dwengo-1-docs": { "resolved": "docs", "link": true @@ -5053,6 +5123,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "license": "MIT" + }, "node_modules/isexe": { "version": "2.0.0", "license": "ISC" @@ -5820,6 +5896,8 @@ }, "node_modules/mime-types": { "version": "3.0.1", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.1.tgz", + "integrity": "sha512-xRc4oEhT6eaBpU1XF7AjpOFD+xQmXNB5OVKwp4tqCuBpHLS/ZbBDrc07mYTDqVMg6PfxUjjNp85O6Cd2Z/5HWA==", "license": "MIT", "dependencies": { "mime-db": "^1.54.0" @@ -6165,6 +6243,12 @@ "node": ">=6" } }, + "node_modules/node-int64": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/node-int64/-/node-int64-0.4.0.tgz", + "integrity": "sha512-O5lz91xSOeoXP6DulyHfllpq+Eg00MWitZIbtPfoSEvqIHdl5gfcY6hYzDWnj0qD5tz52PI08u9qUvSVeUBeHw==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.19", "dev": true, @@ -6843,6 +6927,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", + "license": "MIT" + }, "node_modules/promise-inflight": { "version": "1.0.1", "license": "ISC", @@ -8411,6 +8501,19 @@ "node": ">= 0.8" } }, + "node_modules/unzipper": { + "version": "0.12.3", + "resolved": "https://registry.npmjs.org/unzipper/-/unzipper-0.12.3.tgz", + "integrity": "sha512-PZ8hTS+AqcGxsaQntl3IRBw65QrBI6lxzqDEL7IAo/XCEqRTKGfOX56Vea5TH9SZczRVxuzk1re04z/YjuYCJA==", + "license": "MIT", + "dependencies": { + "bluebird": "~3.7.2", + "duplexer2": "~0.1.4", + "fs-extra": "^11.2.0", + "graceful-fs": "^4.2.2", + "node-int64": "^0.4.0" + } + }, "node_modules/update-browserslist-db": { "version": "1.1.3", "dev": true, From 78353d6b656dc83da2e519ca846835cd87afb601 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 5 May 2025 23:38:18 +0200 Subject: [PATCH 025/117] feat(backend): Controller en route voor het aanmaken van leerobjecten aangemaakt. --- backend/package.json | 2 + backend/src/controllers/learning-objects.ts | 8 +++ backend/src/routes/learning-objects.ts | 11 +++- .../learning-object-service.ts | 3 +- .../learning-object-zip-processing-service.ts | 9 ++-- package-lock.json | 54 +++++++++++++++++++ 6 files changed, 81 insertions(+), 6 deletions(-) diff --git a/backend/package.json b/backend/package.json index 7b2ab878..a95cd334 100644 --- a/backend/package.json +++ b/backend/package.json @@ -31,6 +31,7 @@ "cross-env": "^7.0.3", "dotenv": "^16.4.7", "express": "^5.0.1", + "express-fileupload": "^1.5.1", "express-jwt": "^8.5.1", "gift-pegjs": "^1.0.2", "isomorphic-dompurify": "^2.22.0", @@ -51,6 +52,7 @@ "@mikro-orm/cli": "6.4.12", "@types/cors": "^2.8.17", "@types/express": "^5.0.0", + "@types/express-fileupload": "^1.5.1", "@types/js-yaml": "^4.0.9", "@types/node": "^22.13.4", "@types/response-time": "^2.3.8", diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 83aa33f9..3622912c 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -7,6 +7,7 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { envVars, getEnvVar } from '../util/envVars.js'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import {UploadedFile} from "express-fileupload"; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { if (!req.params.hruid) { @@ -72,3 +73,10 @@ export async function getAttachment(req: Request, res: Response): Promise } res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); } + +export async function handlePostLearningObject(req: Request, res: Response): Promise { + if (!req.files || !req.files[0]) { + throw new BadRequestException('No file uploaded'); + } + await learningObjectService.storeLearningObject((req.files[0] as UploadedFile).tempFilePath); +} diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index 7532765b..254d7ebc 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -1,8 +1,15 @@ import express from 'express'; -import { getAllLearningObjects, getAttachment, getLearningObject, getLearningObjectHTML } from '../controllers/learning-objects.js'; +import { + getAllLearningObjects, + getAttachment, + getLearningObject, + getLearningObjectHTML, + handlePostLearningObject +} from '../controllers/learning-objects.js'; import submissionRoutes from './submissions.js'; import questionRoutes from './questions.js'; +import fileUpload from "express-fileupload"; const router = express.Router(); @@ -18,6 +25,8 @@ const router = express.Router(); // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie router.get('/', getAllLearningObjects); +router.post('/', fileUpload({useTempFiles: true}), handlePostLearningObject) + // Parameter: hruid of learning object // Query: language // Route to fetch data of one learning object based on its hruid diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 4f5409fd..9a0912ae 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -9,6 +9,7 @@ import { } from '@dwengo-1/common/interfaces/learning-content'; import {getLearningObjectRepository} from "../../data/repositories"; import {processLearningObjectZip} from "./learning-object-zip-processing-service"; +import {BadRequestException} from "../../exceptions/bad-request-exception"; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -58,7 +59,7 @@ const learningObjectService = { const learningObject = await processLearningObjectZip(learningObjectPath); if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { - throw Error("Learning object name must start with the user content prefix!"); + throw new BadRequestException("Learning object name must start with the user content prefix!"); } await learningObjectRepository.save(learningObject, {preventOverwrite: true}); diff --git a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts index 3dbd5915..fcf80e2b 100644 --- a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts +++ b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts @@ -1,8 +1,9 @@ import unzipper from 'unzipper'; import mime from 'mime-types'; -import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; import {LearningObject} from "../../entities/content/learning-object.entity"; import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; +import {BadRequestException} from "../../exceptions/bad-request-exception"; +import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; /** * Process an uploaded zip file and construct a LearningObject from its contents. @@ -20,7 +21,7 @@ export async function processLearningObjectZip(filePath: string): Promise=10.16.0" + } + }, "node_modules/bytes": { "version": "3.1.2", "license": "MIT", @@ -4235,6 +4269,18 @@ "url": "https://opencollective.com/express" } }, + "node_modules/express-fileupload": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/express-fileupload/-/express-fileupload-1.5.1.tgz", + "integrity": "sha512-LsYG1ALXEB7vlmjuSw8ABeOctMp8a31aUC5ZF55zuz7O2jLFnmJYrCv10py357ky48aEoBQ/9bVXgFynjvaPmA==", + "license": "MIT", + "dependencies": { + "busboy": "^1.6.0" + }, + "engines": { + "node": ">=12.0.0" + } + }, "node_modules/express-jwt": { "version": "8.5.1", "license": "MIT", @@ -7798,6 +7844,14 @@ "dev": true, "license": "MIT" }, + "node_modules/streamsearch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/streamsearch/-/streamsearch-1.1.0.tgz", + "integrity": "sha512-Mcc5wHehp9aXz1ax6bZUyY5afg9u2rv5cqQI3mRrYkGC8rW2hM02jWuwjtL++LS5qinSyhj2QfLyNsuc+VsExg==", + "engines": { + "node": ">=10.0.0" + } + }, "node_modules/string_decoder": { "version": "1.3.0", "license": "MIT", From c054eb9335a7dddce42e2ef51cff063a808b04ce Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Fri, 9 May 2025 18:08:44 +0200 Subject: [PATCH 026/117] fix: teacher invitations middelware en questions --- .../src/middleware/auth/checks/class-auth-checks.ts | 11 +++++++++++ backend/src/routes/answers.ts | 4 ++-- backend/src/routes/classes.ts | 4 ++-- backend/src/routes/questions.ts | 6 +++--- 4 files changed, 18 insertions(+), 7 deletions(-) diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 7093b0d1..e85aaf3b 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -3,6 +3,7 @@ import { AuthenticationInfo } from '../authentication-info.js'; import { AuthenticatedRequest } from '../authenticated-request.js'; import { fetchClass } from '../../../services/classes.js'; import { mapToUsername } from '../../../interfaces/user.js'; +import {getAllInvitations} from "../../../services/teacher-invitations"; async function teaches(teacherUsername: string, classId: string): Promise { const clazz = await fetchClass(classId); @@ -44,6 +45,16 @@ export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req return clazz.students.map(mapToUsername).includes(auth.username); }); +export const onlyAllowIfInClassOrInvited = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const classId = req.params.classId ?? req.params.classid ?? req.params.id; + const clazz = await fetchClass(classId); + if (auth.accountType === 'teacher') { + const invitations = await getAllInvitations(auth.username, false); + return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some(invitation => invitation.classId == classId); + } + return clazz.students.map(mapToUsername).includes(auth.username); +}); + /** * Only allows the request to pass if the 'class' property in its body is a class the current user is a member of. */ diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index e0cf5b17..0944095f 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,11 +1,11 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; -import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; +import {adminOnly, authenticatedOnly, teachersOnly} from '../middleware/auth/checks/auth-checks.js'; import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; const router = express.Router({ mergeParams: true }); -router.get('/', adminOnly, getAllAnswersHandler); +router.get('/', authenticatedOnly, getAllAnswersHandler); router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler); diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index 4b272971..7602abd5 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -15,7 +15,7 @@ import { } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; -import { onlyAllowIfInClass } from '../middleware/auth/checks/class-auth-checks.js'; +import {onlyAllowIfInClass, onlyAllowIfInClassOrInvited} from '../middleware/auth/checks/class-auth-checks.js'; const router = express.Router(); @@ -23,7 +23,7 @@ router.get('/', adminOnly, getAllClassesHandler); router.post('/', teachersOnly, createClassHandler); -router.get('/:id', onlyAllowIfInClass, getClassHandler); +router.get('/:id', onlyAllowIfInClassOrInvited, getClassHandler); router.put('/:id', teachersOnly, onlyAllowIfInClass, putClassHandler); diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index 76ec4eaa..c4ffa442 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,7 +1,7 @@ import express from 'express'; import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; -import { adminOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; +import {adminOnly, authenticatedOnly, studentsOnly} from '../middleware/auth/checks/auth-checks.js'; import { updateAnswerHandler } from '../controllers/answers.js'; import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; @@ -10,9 +10,9 @@ const router = express.Router({ mergeParams: true }); // Query language // Root endpoint used to search objects -router.get('/', adminOnly, getAllQuestionsHandler); +router.get('/', authenticatedOnly, getAllQuestionsHandler); -router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); +router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); // TODO part of group // Information about a question with id router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler); From 17460684aae3f162919db6980268f1c289f18824 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Fri, 9 May 2025 18:09:22 +0200 Subject: [PATCH 027/117] fix: invalideren cache key bij klas --- frontend/src/queries/classes.ts | 3 +++ frontend/src/queries/students.ts | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index dca92230..d97d978a 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -15,6 +15,7 @@ import { invalidateAllGroupKeys } from "./groups"; import { invalidateAllSubmissionKeys } from "./submissions"; import type { TeachersResponse } from "@/controllers/teachers"; import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; +import {studentClassesQueryKey} from "@/queries/students.ts"; const classController = new ClassController(); @@ -171,6 +172,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType< await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) }); await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) }); + await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, false) }); + await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, true) }); }, }); } diff --git a/frontend/src/queries/students.ts b/frontend/src/queries/students.ts index da87d28b..1d6794f1 100644 --- a/frontend/src/queries/students.ts +++ b/frontend/src/queries/students.ts @@ -33,7 +33,7 @@ function studentsQueryKey(full: boolean): [string, boolean] { function studentQueryKey(username: string): [string, string] { return ["student", username]; } -function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { +export function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { return ["student-classes", username, full]; } function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] { From 42d84c56e508f2cc17667fd14531955a76578c42 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Fri, 9 May 2025 18:10:03 +0200 Subject: [PATCH 028/117] fix: error melding --- frontend/src/views/classes/ClassDisplay.vue | 19 +++ frontend/src/views/classes/SingleClass.vue | 8 +- frontend/src/views/classes/StudentClasses.vue | 2 +- frontend/src/views/classes/TeacherClasses.vue | 113 ++++++++---------- 4 files changed, 77 insertions(+), 65 deletions(-) create mode 100644 frontend/src/views/classes/ClassDisplay.vue diff --git a/frontend/src/views/classes/ClassDisplay.vue b/frontend/src/views/classes/ClassDisplay.vue new file mode 100644 index 00000000..f24293bc --- /dev/null +++ b/frontend/src/views/classes/ClassDisplay.vue @@ -0,0 +1,19 @@ + + + + + diff --git a/frontend/src/views/classes/SingleClass.vue b/frontend/src/views/classes/SingleClass.vue index 5cc62043..21a3ec5a 100644 --- a/frontend/src/views/classes/SingleClass.vue +++ b/frontend/src/views/classes/SingleClass.vue @@ -76,7 +76,7 @@ }, onError: (e) => { dialog.value = false; - showSnackbar(t("failed") + ": " + e.message, "error"); + showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); }, }, ); @@ -104,7 +104,7 @@ } }, onError: (e) => { - showSnackbar(t("failed") + ": " + e.message, "error"); + showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); }, }, ); @@ -125,7 +125,9 @@ usernameTeacher.value = ""; }, onError: (e) => { - showSnackbar(t("failed") + ": " + e.message, "error"); + console.log("error", e) + console.log(e.response.data.error) + showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); }, }); } diff --git a/frontend/src/views/classes/StudentClasses.vue b/frontend/src/views/classes/StudentClasses.vue index 4d7cc369..a972069d 100644 --- a/frontend/src/views/classes/StudentClasses.vue +++ b/frontend/src/views/classes/StudentClasses.vue @@ -99,7 +99,7 @@ showSnackbar(t("sent"), "success"); }, onError: (e) => { - showSnackbar(t("failed") + ": " + e.message, "error"); + showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); }, }, ); diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index 3d6c4db9..2b107575 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -15,6 +15,7 @@ useTeacherInvitationsReceivedQuery, } from "@/queries/teacher-invitations"; import { useDisplay } from "vuetify"; + import ClassDisplay from "@/views/classes/ClassDisplay.vue"; const { t } = useI18n(); @@ -40,7 +41,6 @@ // Fetch all classes of the logged in teacher const classesQuery = useTeacherClassesQuery(username, true); - const allClassesQuery = useClassesQuery(); const { mutate } = useCreateClassMutation(); const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username); const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation(); @@ -69,7 +69,7 @@ await getInvitationsQuery.refetch(); }, onError: (e) => { - showSnackbar(t("failed") + ": " + e.message, "error"); + showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); }, }); } @@ -338,66 +338,57 @@ :query-result="getInvitationsQuery" v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" > - - - - {{ - (classesResponse.data.classes as ClassDTO[]).filter( - (c) => c.id == i.classId, - )[0].displayName - }} - - - {{ - (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName - }} - - - -
- - {{ t("accept") }} - - - {{ t("deny") }} - -
-
- -
- - - -
- - -
+ + + + + {{ + (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName + }} + + + +
+ + {{ t("accept") }} + + + {{ t("deny") }} + +
+
+ +
+ + + +
+ + From a5e4f2437b51acd52e7b037681f387ee53b781ca Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Fri, 9 May 2025 18:20:06 +0200 Subject: [PATCH 029/117] fix: lint --- backend/src/middleware/auth/checks/class-auth-checks.ts | 2 +- backend/src/routes/answers.ts | 2 +- backend/src/routes/questions.ts | 4 ++-- backend/src/services/questions.ts | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index e85aaf3b..6af44827 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -50,7 +50,7 @@ export const onlyAllowIfInClassOrInvited = authorize(async (auth: Authentication const clazz = await fetchClass(classId); if (auth.accountType === 'teacher') { const invitations = await getAllInvitations(auth.username, false); - return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some(invitation => invitation.classId == classId); + return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some(invitation => invitation.classId === classId); } return clazz.students.map(mapToUsername).includes(auth.username); }); diff --git a/backend/src/routes/answers.ts b/backend/src/routes/answers.ts index 0944095f..58179197 100644 --- a/backend/src/routes/answers.ts +++ b/backend/src/routes/answers.ts @@ -1,6 +1,6 @@ import express from 'express'; import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.js'; -import {adminOnly, authenticatedOnly, teachersOnly} from '../middleware/auth/checks/auth-checks.js'; +import { authenticatedOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; import { onlyAllowAuthor, onlyAllowAuthorRequestAnswer, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; const router = express.Router({ mergeParams: true }); diff --git a/backend/src/routes/questions.ts b/backend/src/routes/questions.ts index c4ffa442..6cad3c01 100644 --- a/backend/src/routes/questions.ts +++ b/backend/src/routes/questions.ts @@ -1,7 +1,7 @@ import express from 'express'; import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js'; import answerRoutes from './answers.js'; -import {adminOnly, authenticatedOnly, studentsOnly} from '../middleware/auth/checks/auth-checks.js'; +import { authenticatedOnly, studentsOnly } from '../middleware/auth/checks/auth-checks.js'; import { updateAnswerHandler } from '../controllers/answers.js'; import { onlyAllowAuthor, onlyAllowAuthorRequest, onlyAllowIfHasAccessToQuestion } from '../middleware/auth/checks/question-checks.js'; @@ -12,7 +12,7 @@ const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects router.get('/', authenticatedOnly, getAllQuestionsHandler); -router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); // TODO part of group +router.post('/', studentsOnly, onlyAllowAuthor, createQuestionHandler); // Information about a question with id router.get('/:seq', onlyAllowIfHasAccessToQuestion, getQuestionHandler); diff --git a/backend/src/services/questions.ts b/backend/src/services/questions.ts index 90b5d7dd..c6d978d8 100644 --- a/backend/src/services/questions.ts +++ b/backend/src/services/questions.ts @@ -111,7 +111,7 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat const question = await questionRepository.createQuestion({ loId, author, - inGroup: inGroup!, + inGroup: inGroup, content, }); From 447fd150dac9199618ef937e06e5618e60a41a87 Mon Sep 17 00:00:00 2001 From: Lint Action Date: Fri, 9 May 2025 16:21:18 +0000 Subject: [PATCH 030/117] style: fix linting issues met Prettier --- .../auth/checks/class-auth-checks.ts | 4 ++-- backend/src/routes/auth.ts | 2 +- backend/src/routes/classes.ts | 2 +- frontend/src/queries/classes.ts | 2 +- frontend/src/views/classes/ClassDisplay.vue | 23 ++++++++++--------- frontend/src/views/classes/SingleClass.vue | 4 ++-- frontend/src/views/classes/TeacherClasses.vue | 4 +--- 7 files changed, 20 insertions(+), 21 deletions(-) diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 6af44827..bc213796 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -3,7 +3,7 @@ import { AuthenticationInfo } from '../authentication-info.js'; import { AuthenticatedRequest } from '../authenticated-request.js'; import { fetchClass } from '../../../services/classes.js'; import { mapToUsername } from '../../../interfaces/user.js'; -import {getAllInvitations} from "../../../services/teacher-invitations"; +import { getAllInvitations } from '../../../services/teacher-invitations'; async function teaches(teacherUsername: string, classId: string): Promise { const clazz = await fetchClass(classId); @@ -50,7 +50,7 @@ export const onlyAllowIfInClassOrInvited = authorize(async (auth: Authentication const clazz = await fetchClass(classId); if (auth.accountType === 'teacher') { const invitations = await getAllInvitations(auth.username, false); - return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some(invitation => invitation.classId === classId); + return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some((invitation) => invitation.classId === classId); } return clazz.students.map(mapToUsername).includes(auth.username); }); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index b71b418d..cc20bb75 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -5,7 +5,7 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut const router = express.Router(); // Returns auth configuration for frontend -router.get('/config', handleGetFrontendAuthConfig) +router.get('/config', handleGetFrontendAuthConfig); router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ diff --git a/backend/src/routes/classes.ts b/backend/src/routes/classes.ts index 7602abd5..8a35eb2a 100644 --- a/backend/src/routes/classes.ts +++ b/backend/src/routes/classes.ts @@ -15,7 +15,7 @@ import { } from '../controllers/classes.js'; import assignmentRouter from './assignments.js'; import { adminOnly, teachersOnly } from '../middleware/auth/checks/auth-checks.js'; -import {onlyAllowIfInClass, onlyAllowIfInClassOrInvited} from '../middleware/auth/checks/class-auth-checks.js'; +import { onlyAllowIfInClass, onlyAllowIfInClassOrInvited } from '../middleware/auth/checks/class-auth-checks.js'; const router = express.Router(); diff --git a/frontend/src/queries/classes.ts b/frontend/src/queries/classes.ts index d97d978a..6c452f10 100644 --- a/frontend/src/queries/classes.ts +++ b/frontend/src/queries/classes.ts @@ -15,7 +15,7 @@ import { invalidateAllGroupKeys } from "./groups"; import { invalidateAllSubmissionKeys } from "./submissions"; import type { TeachersResponse } from "@/controllers/teachers"; import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; -import {studentClassesQueryKey} from "@/queries/students.ts"; +import { studentClassesQueryKey } from "@/queries/students.ts"; const classController = new ClassController(); diff --git a/frontend/src/views/classes/ClassDisplay.vue b/frontend/src/views/classes/ClassDisplay.vue index f24293bc..96bc8ea1 100644 --- a/frontend/src/views/classes/ClassDisplay.vue +++ b/frontend/src/views/classes/ClassDisplay.vue @@ -1,19 +1,20 @@ - - diff --git a/frontend/src/views/classes/SingleClass.vue b/frontend/src/views/classes/SingleClass.vue index 21a3ec5a..bb0b6f71 100644 --- a/frontend/src/views/classes/SingleClass.vue +++ b/frontend/src/views/classes/SingleClass.vue @@ -125,8 +125,8 @@ usernameTeacher.value = ""; }, onError: (e) => { - console.log("error", e) - console.log(e.response.data.error) + console.log("error", e); + console.log(e.response.data.error); showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); }, }); diff --git a/frontend/src/views/classes/TeacherClasses.vue b/frontend/src/views/classes/TeacherClasses.vue index 2b107575..d6b0fc12 100644 --- a/frontend/src/views/classes/TeacherClasses.vue +++ b/frontend/src/views/classes/TeacherClasses.vue @@ -346,9 +346,7 @@ - {{ - (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName - }} + {{ (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName }} From 6600441b08a62f69230f18c9c77087fbe664b7b2 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Sun, 11 May 2025 15:46:53 +0200 Subject: [PATCH 031/117] feat(backend): opvragen van leerobjecten van een leerkracht --- backend/src/controllers/learning-objects.ts | 36 +++++++---- .../content/learning-object-repository.ts | 4 +- .../content/learning-object.entity.ts | 16 ++++- .../database-learning-object-provider.ts | 11 ++++ .../dwengo-api-learning-object-provider.ts | 7 +++ .../learning-object-provider.ts | 5 ++ .../learning-object-service.ts | 28 ++++++++- .../learning-object-zip-processing-service.ts | 59 +++++++++++++------ backend/src/services/teachers.ts | 2 +- .../OwnLearningContentPage.vue | 11 ++++ .../OwnLearningObjectsView.vue | 11 ++++ 11 files changed, 152 insertions(+), 38 deletions(-) create mode 100644 frontend/src/views/own-learning-content/OwnLearningContentPage.vue create mode 100644 frontend/src/views/own-learning-content/OwnLearningObjectsView.vue diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 3622912c..ba8fdef3 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -8,6 +8,7 @@ import { NotFoundException } from '../exceptions/not-found-exception.js'; import { envVars, getEnvVar } from '../util/envVars.js'; import { FilteredLearningObject, LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; import {UploadedFile} from "express-fileupload"; +import {AuthenticatedRequest} from "../middleware/auth/authenticated-request"; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifierDTO { if (!req.params.hruid) { @@ -31,17 +32,24 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif } export async function getAllLearningObjects(req: Request, res: Response): Promise { - const learningPathId = getLearningPathIdentifierFromRequest(req); - const full = req.query.full; + if (req.query.admin) { // If the admin query parameter is present, the user wants to have all learning objects with this admin. + const learningObjects = + await learningObjectService.getLearningObjectsAdministratedBy(req.query.admin as string); - let learningObjects: FilteredLearningObject[] | string[]; - if (full) { - learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId); - } else { - learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); + res.json(learningObjects); + } else { // Else he/she wants all learning objects on the path specified by the request parameters. + const learningPathId = getLearningPathIdentifierFromRequest(req); + const full = req.query.full; + + let learningObjects: FilteredLearningObject[] | string[]; + if (full) { + learningObjects = await learningObjectService.getLearningObjectsFromPath(learningPathId); + } else { + learningObjects = await learningObjectService.getLearningObjectIdsFromPath(learningPathId); + } + + res.json({ learningObjects: learningObjects }); } - - res.json({ learningObjects: learningObjects }); } export async function getLearningObject(req: Request, res: Response): Promise { @@ -74,9 +82,13 @@ export async function getAttachment(req: Request, res: Response): Promise res.setHeader('Content-Type', attachment.mimeType).send(attachment.content); } -export async function handlePostLearningObject(req: Request, res: Response): Promise { - if (!req.files || !req.files[0]) { +export async function handlePostLearningObject(req: AuthenticatedRequest, res: Response): Promise { + if (!req.files || !req.files.learningObject) { throw new BadRequestException('No file uploaded'); } - await learningObjectService.storeLearningObject((req.files[0] as UploadedFile).tempFilePath); + const learningObject = await learningObjectService.storeLearningObject( + (req.files.learningObject as UploadedFile).tempFilePath, + [req.auth!.username] + ); + res.json(learningObject); } diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 889a1594..d11833dc 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -33,9 +33,9 @@ export class LearningObjectRepository extends DwengoEntityRepository { + public async findAllByAdmin(adminUsername: string): Promise { return this.find( - { admins: teacher }, + { admins: { $contains: adminUsername } }, { populate: ['admins'] } // Make sure to load admin relations ); } diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index e0ae09d6..825bf744 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,4 +1,14 @@ -import { ArrayType, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { + ArrayType, + Collection, + Embedded, + Entity, + Enum, + ManyToMany, + OneToMany, + PrimaryKey, + Property +} from '@mikro-orm/core'; import { Attachment } from './attachment.entity.js'; import { Teacher } from '../users/teacher.entity.js'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; @@ -28,7 +38,7 @@ export class LearningObject { @ManyToMany({ entity: () => Teacher, }) - admins!: Teacher[]; + admins: Collection = new Collection(this); @Property({ type: 'string' }) title!: string; @@ -84,7 +94,7 @@ export class LearningObject { entity: () => Attachment, mappedBy: 'learningObject', }) - attachments: Attachment[] = []; + attachments: Collection = new Collection(this); @Property({ type: 'blob' }) content!: Buffer; diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index 0b805a56..9d16d820 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -109,6 +109,17 @@ const databaseLearningObjectProvider: LearningObjectProvider = { ); return learningObjects.filter((it) => it !== null); }, + + /** + * Returns all learning objects containing the given username as an admin. + */ + async getLearningObjectsAdministratedBy(adminUsername: string): Promise { + const learningObjectRepo = getLearningObjectRepository(); + const learningObjects = await learningObjectRepo.findAllByAdmin(adminUsername); + return learningObjects + .map(it => convertLearningObject(it)) + .filter(it => it != null); + } }; export default databaseLearningObjectProvider; diff --git a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts index 4a4bdc54..804d2d20 100644 --- a/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts +++ b/backend/src/services/learning-objects/dwengo-api-learning-object-provider.ts @@ -135,6 +135,13 @@ const dwengoApiLearningObjectProvider: LearningObjectProvider = { return html; }, + + /** + * Obtain all learning objects who have the user with the given username as an admin. + */ + async getLearningObjectsAdministratedBy(_adminUsername: string): Promise { + return []; // The dwengo database does not contain any learning objects administrated by users. + } }; export default dwengoApiLearningObjectProvider; diff --git a/backend/src/services/learning-objects/learning-object-provider.ts b/backend/src/services/learning-objects/learning-object-provider.ts index 14848bc0..69ad268d 100644 --- a/backend/src/services/learning-objects/learning-object-provider.ts +++ b/backend/src/services/learning-objects/learning-object-provider.ts @@ -20,4 +20,9 @@ export interface LearningObjectProvider { * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ getLearningObjectHTML(id: LearningObjectIdentifierDTO): Promise; + + /** + * Obtain all learning object who have the user with the given username as an admin. + */ + getLearningObjectsAdministratedBy(username: string): Promise; } diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 9a0912ae..9d6c6673 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -7,9 +7,10 @@ import { LearningObjectIdentifierDTO, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; -import {getLearningObjectRepository} from "../../data/repositories"; +import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories"; import {processLearningObjectZip} from "./learning-object-zip-processing-service"; import {BadRequestException} from "../../exceptions/bad-request-exception"; +import {LearningObject} from "../../entities/content/learning-object.entity"; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -50,19 +51,40 @@ const learningObjectService = { return getProvider(id).getLearningObjectHTML(id); }, + /** + * Obtain all learning objects administrated by the user with the given username. + */ + async getLearningObjectsAdministratedBy(adminUsername: string): Promise { + return databaseLearningObjectProvider.getLearningObjectsAdministratedBy(adminUsername); + }, /** * Store the learning object in the given zip file in the database. + * @param learningObjectPath The path where the uploaded learning object resides. + * @param admins The usernames of the users which should be administrators of the learning object. */ - async storeLearningObject(learningObjectPath: string): Promise { + async storeLearningObject(learningObjectPath: string, admins: string[]): Promise { const learningObjectRepository = getLearningObjectRepository(); const learningObject = await processLearningObjectZip(learningObjectPath); + console.log(learningObject); if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { - throw new BadRequestException("Learning object name must start with the user content prefix!"); + learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; } + // Lookup the admin teachers based on their usernames and add them to the admins of the learning object. + const teacherRepo = getTeacherRepository(); + const adminTeachers = await Promise.all( + admins.map(it => teacherRepo.findByUsername(it)) + ); + adminTeachers.forEach(it => { + if (it != null) { + learningObject.admins.add(it); + } + }); + await learningObjectRepository.save(learningObject, {preventOverwrite: true}); + return learningObject; } }; diff --git a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts index fcf80e2b..53c4ca9c 100644 --- a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts +++ b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts @@ -5,6 +5,9 @@ import {getAttachmentRepository, getLearningObjectRepository} from "../../data/r import {BadRequestException} from "../../exceptions/bad-request-exception"; import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; +const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; +const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; + /** * Process an uploaded zip file and construct a LearningObject from its contents. * @param filePath Path of the zip file to process. @@ -15,40 +18,62 @@ export async function processLearningObjectZip(filePath: string): Promise attachmentRepo.create({ name: it.name, content: it.content, mimeType: mime.lookup(it.name) || "text/plain", learningObject })) - learningObject.attachments.push(...attachmentEntities); + attachmentEntities.forEach(it => learningObject.attachments.add(it)); return learningObject; } diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index 4fdb15be..8f41df8b 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -124,7 +124,7 @@ export async function getTeacherQuestions(username: string, full: boolean): Prom // Find all learning objects that this teacher manages const learningObjectRepository: LearningObjectRepository = getLearningObjectRepository(); - const learningObjects: LearningObject[] = await learningObjectRepository.findAllByTeacher(teacher); + const learningObjects: LearningObject[] = await learningObjectRepository.findAllByAdmin(teacher); if (!learningObjects || learningObjects.length === 0) { return []; diff --git a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue new file mode 100644 index 00000000..85af7153 --- /dev/null +++ b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue @@ -0,0 +1,11 @@ + + + + + diff --git a/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue new file mode 100644 index 00000000..85af7153 --- /dev/null +++ b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue @@ -0,0 +1,11 @@ + + + + + From be1091544c415283dd7e620d82d3abf4a84cd939 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 12 May 2025 00:47:37 +0200 Subject: [PATCH 032/117] feat(frontend): basisimplementatie leerobject upload-UI --- .../content/learning-object-repository.ts | 7 +- .../learning-object-service.ts | 6 +- .../learning-object-zip-processing-service.ts | 67 +++++++++------- frontend/src/controllers/base-controller.ts | 22 ++++++ frontend/src/controllers/learning-objects.ts | 8 ++ frontend/src/queries/learning-objects.ts | 33 ++++++-- frontend/src/router/index.ts | 7 ++ .../LearningObjectUploadButton.vue | 76 +++++++++++++++++++ .../OwnLearningContentPage.vue | 43 ++++++++++- .../OwnLearningObjectsView.vue | 73 +++++++++++++++++- .../OwnLearningPathsView.vue | 9 +++ 11 files changed, 311 insertions(+), 40 deletions(-) create mode 100644 frontend/src/views/own-learning-content/LearningObjectUploadButton.vue create mode 100644 frontend/src/views/own-learning-content/OwnLearningPathsView.vue diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index d11833dc..1dd7c9e0 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -2,7 +2,6 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; import { Language } from '@dwengo-1/common/util/language'; -import { Teacher } from '../../entities/users/teacher.entity.js'; export class LearningObjectRepository extends DwengoEntityRepository { public async findByIdentifier(identifier: LearningObjectIdentifier): Promise { @@ -35,7 +34,11 @@ export class LearningObjectRepository extends DwengoEntityRepository { return this.find( - { admins: { $contains: adminUsername } }, + { + admins: { + username: adminUsername + } + }, { populate: ['admins'] } // Make sure to load admin relations ); } diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 9d6c6673..0a229fde 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -9,7 +9,6 @@ import { } from '@dwengo-1/common/interfaces/learning-content'; import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories"; import {processLearningObjectZip} from "./learning-object-zip-processing-service"; -import {BadRequestException} from "../../exceptions/bad-request-exception"; import {LearningObject} from "../../entities/content/learning-object.entity"; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { @@ -67,7 +66,6 @@ const learningObjectService = { const learningObjectRepository = getLearningObjectRepository(); const learningObject = await processLearningObjectZip(learningObjectPath); - console.log(learningObject); if (!learningObject.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { learningObject.hruid = getEnvVar(envVars.UserContentPrefix) + learningObject.hruid; } @@ -75,10 +73,10 @@ const learningObjectService = { // Lookup the admin teachers based on their usernames and add them to the admins of the learning object. const teacherRepo = getTeacherRepository(); const adminTeachers = await Promise.all( - admins.map(it => teacherRepo.findByUsername(it)) + admins.map(async it => teacherRepo.findByUsername(it)) ); adminTeachers.forEach(it => { - if (it != null) { + if (it !== null) { learningObject.admins.add(it); } }); diff --git a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts index 53c4ca9c..213c3f17 100644 --- a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts +++ b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts @@ -4,6 +4,7 @@ import {LearningObject} from "../../entities/content/learning-object.entity"; import {getAttachmentRepository, getLearningObjectRepository} from "../../data/repositories"; import {BadRequestException} from "../../exceptions/bad-request-exception"; import {LearningObjectMetadata} from "@dwengo-1/common/dist/interfaces/learning-content"; +import { ReturnValue } from '../../entities/content/return-value.entity'; const METADATA_PATH_REGEX = /.*[/^]metadata\.json$/; const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; @@ -13,33 +14,38 @@ const CONTENT_PATH_REGEX = /.*[/^]content\.[a-zA-Z]*$/; * @param filePath Path of the zip file to process. */ export async function processLearningObjectZip(filePath: string): Promise { - const learningObjectRepo = getLearningObjectRepository(); - const attachmentRepo = getAttachmentRepository(); + let zip: unzipper.CentralDirectory; + try { + zip = await unzipper.Open.file(filePath); + } catch(_: unknown) { + throw new BadRequestException("invalid_zip"); + } - const zip = await unzipper.Open.file(filePath); let metadata: LearningObjectMetadata | undefined = undefined; const attachments: {name: string, content: Buffer}[] = []; let content: Buffer | undefined = undefined; - if (zip.files.length == 0) { + if (zip.files.length === 0) { throw new BadRequestException("empty_zip") } - for (const file of zip.files) { - if (file.type !== "Directory") { - if (METADATA_PATH_REGEX.test(file.path)) { - metadata = await processMetadataJson(file); - } else if (CONTENT_PATH_REGEX.test(file.path)) { - content = await processFile(file); - } else { - attachments.push({ - name: file.path, - content: await processFile(file) - }); + await Promise.all( + zip.files.map(async file => { + if (file.type !== "Directory") { + if (METADATA_PATH_REGEX.test(file.path)) { + metadata = await processMetadataJson(file); + } else if (CONTENT_PATH_REGEX.test(file.path)) { + content = await processFile(file); + } else { + attachments.push({ + name: file.path, + content: await processFile(file) + }); + } } - } - } + }) + ); if (!metadata) { throw new BadRequestException("missing_metadata"); @@ -49,20 +55,30 @@ export async function processLearningObjectZip(filePath: string): Promise learningObject.attachments.add(it)); - + })); + attachmentEntities.forEach(it => { learningObject.attachments.add(it); }); return learningObject; } diff --git a/frontend/src/controllers/base-controller.ts b/frontend/src/controllers/base-controller.ts index 64f2363d..bb450618 100644 --- a/frontend/src/controllers/base-controller.ts +++ b/frontend/src/controllers/base-controller.ts @@ -37,6 +37,28 @@ export abstract class BaseController { return response.data; } + /** + * Sends a POST-request with a form-data body with the given file. + * + * @param path Relative path in the api to send the request to. + * @param formFieldName The name of the form field in which the file should be. + * @param file The file to upload. + * @param queryParams The query parameters. + * @returns The response the POST request generated. + */ + protected async postFile(path: string, formFieldName: string, file: File, queryParams?: QueryParams): Promise { + const formData = new FormData(); + formData.append(formFieldName, file); + const response = await apiClient.post(this.absolutePathFor(path), formData, { + params: queryParams, + headers: { + 'Content-Type': 'multipart/form-data' + } + }); + BaseController.assertSuccessResponse(response) + return response.data; + } + protected async delete(path: string, queryParams?: QueryParams): Promise { const response = await apiClient.delete(this.absolutePathFor(path), { params: queryParams }); BaseController.assertSuccessResponse(response); diff --git a/frontend/src/controllers/learning-objects.ts b/frontend/src/controllers/learning-objects.ts index d62ba1f4..7239b88f 100644 --- a/frontend/src/controllers/learning-objects.ts +++ b/frontend/src/controllers/learning-objects.ts @@ -14,4 +14,12 @@ export class LearningObjectController extends BaseController { async getHTML(hruid: string, language: Language, version: number): Promise { return this.get(`/${hruid}/html`, { language, version }, "document"); } + + async getAllAdministratedBy(admin: string): Promise { + return this.get("/", { admin }); + } + + async upload(learningObjectZip: File): Promise { + return this.postFile("/", "learningObject", learningObjectZip); + } } diff --git a/frontend/src/queries/learning-objects.ts b/frontend/src/queries/learning-objects.ts index 35ed7ae4..5e7612e9 100644 --- a/frontend/src/queries/learning-objects.ts +++ b/frontend/src/queries/learning-objects.ts @@ -1,9 +1,10 @@ import { type MaybeRefOrGetter, toValue } from "vue"; import type { Language } from "@/data-objects/language.ts"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { getLearningObjectController } from "@/controllers/controllers.ts"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; +import type { AxiosError } from "axios"; export const LEARNING_OBJECT_KEY = "learningObject"; const learningObjectController = getLearningObjectController(); @@ -24,15 +25,15 @@ export function useLearningObjectMetadataQuery( } export function useLearningObjectHTMLQuery( - hruid: MaybeRefOrGetter, - language: MaybeRefOrGetter, - version: MaybeRefOrGetter, + hruid: MaybeRefOrGetter, + language: MaybeRefOrGetter, + version: MaybeRefOrGetter, ): UseQueryReturnType { return useQuery({ queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version], queryFn: async () => { const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)]; - return learningObjectController.getHTML(hruidVal, languageVal, versionVal); + return learningObjectController.getHTML(hruidVal!, languageVal!, versionVal!); }, enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)), }); @@ -55,3 +56,25 @@ export function useLearningObjectListForPathQuery( enabled: () => Boolean(toValue(learningPath)), }); } + +export function useLearningObjectListForAdminQuery( + admin: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin], + queryFn: async () => { + const adminVal = toValue(admin); + return await learningObjectController.getAllAdministratedBy(adminVal!); + }, + enabled: () => toValue(admin) !== undefined + }); +} + +export function useUploadLearningObjectMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip), + onSuccess: async () => { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } + }); +} diff --git a/frontend/src/router/index.ts b/frontend/src/router/index.ts index 359eab1a..c8a1ebc4 100644 --- a/frontend/src/router/index.ts +++ b/frontend/src/router/index.ts @@ -14,6 +14,7 @@ import UserHomePage from "@/views/homepage/UserHomePage.vue"; import SingleTheme from "@/views/SingleTheme.vue"; import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; import authService from "@/services/auth/auth-service"; +import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue"; const router = createRouter({ history: createWebHistory(import.meta.env.BASE_URL), @@ -114,6 +115,12 @@ const router = createRouter({ component: LearningPathSearchPage, meta: { requiresAuth: true }, }, + { + path: "my", + name: "OwnLearningContentPage", + component: OwnLearningContentPage, + meta: { requiresAuth: true } + }, { path: ":hruid/:language/:learningObjectHruid", name: "LearningPath", diff --git a/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue b/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue new file mode 100644 index 00000000..34211467 --- /dev/null +++ b/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue @@ -0,0 +1,76 @@ + + + diff --git a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue index 85af7153..609c8433 100644 --- a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue +++ b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue @@ -1,11 +1,52 @@ diff --git a/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue index 85af7153..831f1cf0 100644 --- a/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue +++ b/frontend/src/views/own-learning-content/OwnLearningObjectsView.vue @@ -1,11 +1,80 @@ diff --git a/frontend/src/views/own-learning-content/OwnLearningPathsView.vue b/frontend/src/views/own-learning-content/OwnLearningPathsView.vue new file mode 100644 index 00000000..b199202e --- /dev/null +++ b/frontend/src/views/own-learning-content/OwnLearningPathsView.vue @@ -0,0 +1,9 @@ + + + + + From a7f90aace38437afc9c8966699bb2c7217b5e30b Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 12 May 2025 14:35:55 +0200 Subject: [PATCH 033/117] feat(backend): Endpoints voor het verwijderen van leerobjecten --- backend/src/controllers/learning-objects.ts | 19 +++++++++++++++++++ .../content/learning-object-repository.ts | 9 +++++++++ .../learning-object-service.ts | 9 +++++++++ 3 files changed, 37 insertions(+) diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index ba8fdef3..967ce355 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -92,3 +92,22 @@ export async function handlePostLearningObject(req: AuthenticatedRequest, res: R ); res.json(learningObject); } + +export async function handleDeleteLearningObject(req: AuthenticatedRequest, res: Response): Promise { + const learningObjectId = getLearningObjectIdentifierFromRequest(req); + + if (!learningObjectId.version) { + throw new BadRequestException("When deleting a learning object, a version must be specified."); + } + + const deletedLearningObject = await learningObjectService.deleteLearningObject({ + hruid: learningObjectId.hruid, + version: learningObjectId.version, + language: learningObjectId.language + }); + if (deletedLearningObject) { + res.json(deletedLearningObject); + } else { + throw new NotFoundException("Learning object not found"); + } +} diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 1dd7c9e0..889370d5 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -42,4 +42,13 @@ export class LearningObjectRepository extends DwengoEntityRepository { + const learningObject = await this.findByIdentifier(identifier); + if (learningObject) { + await this.em.removeAndFlush(learningObject); + } + return learningObject; + } + } diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 0a229fde..4ada879b 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -10,6 +10,7 @@ import { import {getLearningObjectRepository, getTeacherRepository} from "../../data/repositories"; import {processLearningObjectZip} from "./learning-object-zip-processing-service"; import {LearningObject} from "../../entities/content/learning-object.entity"; +import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -83,6 +84,14 @@ const learningObjectService = { await learningObjectRepository.save(learningObject, {preventOverwrite: true}); return learningObject; + }, + + /** + * Deletes the learning object with the given identifier. + */ + async deleteLearningObject(id: LearningObjectIdentifier): Promise { + const learningObjectRepository = getLearningObjectRepository(); + return await learningObjectRepository.removeByIdentifier(id); } }; From 20c04370b5a9527f94e1b820fede51b6f7688318 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 12 May 2025 14:57:54 +0200 Subject: [PATCH 034/117] feat(backend): Bescherming van leerobject-manipulatie endpoints. Ook delete route voor leerobjecten toegevoegd. --- backend/src/middleware/auth/auth.ts | 13 ++++++++++--- .../auth/checks/learning-object-auth-checks.ts | 16 ++++++++++++++++ backend/src/routes/learning-objects.ts | 11 ++++++++++- .../learning-objects/learning-object-service.ts | 14 ++++++++++++++ 4 files changed, 50 insertions(+), 4 deletions(-) create mode 100644 backend/src/middleware/auth/checks/learning-object-auth-checks.ts diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 73a65b9a..2e1c3765 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -8,6 +8,7 @@ import { AuthenticatedRequest } from './authenticated-request.js'; import { AuthenticationInfo } from './authentication-info.js'; import { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; import { ForbiddenException } from '../../exceptions/forbidden-exception.js'; +import { RequestHandler } from 'express'; const JWKS_CACHE = true; const JWKS_RATE_LIMIT = true; @@ -115,11 +116,17 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; * @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates * to true. */ -export function authorize(accessCondition: (auth: AuthenticationInfo) => boolean) { - return (req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void => { +export function authorize( + accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest) => boolean | Promise +): RequestHandler { + return async ( + req: AuthenticatedRequest, + _res: express.Response, + next: express.NextFunction + ): Promise => { if (!req.auth) { throw new UnauthorizedException(); - } else if (!accessCondition(req.auth)) { + } else if (!(await accessCondition(req.auth, req))) { throw new ForbiddenException(); } else { next(); diff --git a/backend/src/middleware/auth/checks/learning-object-auth-checks.ts b/backend/src/middleware/auth/checks/learning-object-auth-checks.ts new file mode 100644 index 00000000..31387198 --- /dev/null +++ b/backend/src/middleware/auth/checks/learning-object-auth-checks.ts @@ -0,0 +1,16 @@ +import { Language } from "@dwengo-1/common/util/language"; +import learningObjectService from "../../../services/learning-objects/learning-object-service"; +import { authorize } from "../auth"; +import { AuthenticatedRequest } from "../authenticated-request"; +import { AuthenticationInfo } from "../authentication-info"; + +export const onlyAdminsForLearningObject = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const { hruid } = req.params; + const { version, language } = req.query; + const admins = await learningObjectService.getAdmins({ + hruid, + language: language as Language, + version: parseInt(version as string) + }); + return auth.username in admins; +}); diff --git a/backend/src/routes/learning-objects.ts b/backend/src/routes/learning-objects.ts index 254d7ebc..46339ce5 100644 --- a/backend/src/routes/learning-objects.ts +++ b/backend/src/routes/learning-objects.ts @@ -4,12 +4,15 @@ import { getAttachment, getLearningObject, getLearningObjectHTML, + handleDeleteLearningObject, handlePostLearningObject } from '../controllers/learning-objects.js'; import submissionRoutes from './submissions.js'; import questionRoutes from './questions.js'; import fileUpload from "express-fileupload"; +import { teachersOnly } from '../middleware/auth/auth.js'; +import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; const router = express.Router(); @@ -25,7 +28,7 @@ const router = express.Router(); // Example 2: http://localhost:3000/learningObject?full=true&hruid=un_artificiele_intelligentie router.get('/', getAllLearningObjects); -router.post('/', fileUpload({useTempFiles: true}), handlePostLearningObject) +router.post('/', teachersOnly, fileUpload({useTempFiles: true}), handlePostLearningObject) // Parameter: hruid of learning object // Query: language @@ -33,6 +36,12 @@ router.post('/', fileUpload({useTempFiles: true}), handlePostLearningObject) // Example: http://localhost:3000/learningObject/un_ai7 router.get('/:hruid', getLearningObject); +// Parameter: hruid of learning object +// Query: language +// Route to delete a learning object based on its hruid. +// Example: http://localhost:3000/learningObject/un_ai7?language=nl&version=1 +router.delete('/:hruid', onlyAdminsForLearningObject, handleDeleteLearningObject) + router.use('/:hruid/submissions', submissionRoutes); router.use('/:hruid/:version/questions', questionRoutes); diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 4ada879b..f70b88b2 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -11,6 +11,7 @@ import {getLearningObjectRepository, getTeacherRepository} from "../../data/repo import {processLearningObjectZip} from "./learning-object-zip-processing-service"; import {LearningObject} from "../../entities/content/learning-object.entity"; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js'; +import { NotFoundException } from '../../exceptions/not-found-exception.js'; function getProvider(id: LearningObjectIdentifierDTO): LearningObjectProvider { if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { @@ -92,6 +93,19 @@ const learningObjectService = { async deleteLearningObject(id: LearningObjectIdentifier): Promise { const learningObjectRepository = getLearningObjectRepository(); return await learningObjectRepository.removeByIdentifier(id); + }, + + /** + * Returns a list of the usernames of the administrators of the learning object with the given identifier. + * @throws NotFoundException if the specified learning object was not found in the database. + */ + async getAdmins(id: LearningObjectIdentifier): Promise { + const learningObjectRepo = getLearningObjectRepository(); + const learningObject = await learningObjectRepo.findByIdentifier(id); + if (!learningObject) { + throw new NotFoundException("The specified learning object does not exist."); + } + return learningObject.admins.map(admin => admin.username); } }; From 30ca3b70ded19467c1d91769f5338b7b742a18ef Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 12 May 2025 16:11:08 +0200 Subject: [PATCH 035/117] feat(backend): PUSH, PUT en DELETE endpoints voor leerpaden aangemaakt. --- backend/src/controllers/learning-paths.ts | 118 ++++++++++++------ .../data/content/learning-path-repository.ts | 27 ++++ .../auth/checks/learning-path-auth-checks.ts | 12 ++ backend/src/routes/learning-paths.ts | 8 +- .../database-learning-path-provider.ts | 9 ++ .../dwengo-api-learning-path-provider.ts | 4 + .../learning-paths/learning-path-provider.ts | 5 + .../learning-paths/learning-path-service.ts | 47 ++++++- 8 files changed, 186 insertions(+), 44 deletions(-) create mode 100644 backend/src/middleware/auth/checks/learning-path-auth-checks.ts diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 1bd3f2b1..9a03e681 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -7,51 +7,89 @@ import { BadRequestException } from '../exceptions/bad-request-exception.js'; import { NotFoundException } from '../exceptions/not-found-exception.js'; import { Group } from '../entities/assignments/group.entity.js'; import { getAssignmentRepository, getGroupRepository } from '../data/repositories.js'; +import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js'; +import { LearningPath, LearningPathIdentifier } from '@dwengo-1/common/interfaces/learning-content'; +import { getTeacher } from '../services/teachers.js'; /** * Fetch learning paths based on query parameters. */ export async function getLearningPaths(req: Request, res: Response): Promise { - const hruids = req.query.hruid; - const themeKey = req.query.theme as string; - const searchQuery = req.query.search as string; - const language = (req.query.language as string) || FALLBACK_LANG; - - const forGroupNo = req.query.forGroup as string; - const assignmentNo = req.query.assignmentNo as string; - const classId = req.query.classId as string; - - let forGroup: Group | undefined; - - if (forGroupNo) { - if (!assignmentNo || !classId) { - throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); - } - const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo)); - if (assignment) { - forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined; - } - } - - let hruidList; - - if (hruids) { - hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; - } else if (themeKey) { - const theme = themes.find((t) => t.title === themeKey); - if (theme) { - hruidList = theme.hruids; - } else { - throw new NotFoundException(`Theme "${themeKey}" not found.`); - } - } else if (searchQuery) { - const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup); - res.json(searchResults); - return; + const admin = req.query.admin; + if (admin) { + const paths = await learningPathService.getLearningPathsAdministratedBy(admin as string); + res.json(paths); } else { - hruidList = themes.flatMap((theme) => theme.hruids); - } + const hruids = req.query.hruid; + const themeKey = req.query.theme as string; + const searchQuery = req.query.search as string; + const language = (req.query.language as string) || FALLBACK_LANG; - const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); - res.json(learningPaths.data); + const forGroupNo = req.query.forGroup as string; + const assignmentNo = req.query.assignmentNo as string; + const classId = req.query.classId as string; + + let forGroup: Group | undefined; + + if (forGroupNo) { + if (!assignmentNo || !classId) { + throw new BadRequestException('If forGroupNo is specified, assignmentNo and classId must also be specified.'); + } + const assignment = await getAssignmentRepository().findByClassIdAndAssignmentId(classId, parseInt(assignmentNo)); + if (assignment) { + forGroup = (await getGroupRepository().findByAssignmentAndGroupNumber(assignment, parseInt(forGroupNo))) ?? undefined; + } + } + + let hruidList; + + if (hruids) { + hruidList = Array.isArray(hruids) ? hruids.map(String) : [String(hruids)]; + } else if (themeKey) { + const theme = themes.find((t) => t.title === themeKey); + if (theme) { + hruidList = theme.hruids; + } else { + throw new NotFoundException(`Theme "${themeKey}" not found.`); + } + } else if (searchQuery) { + const searchResults = await learningPathService.searchLearningPaths(searchQuery, language as Language, forGroup); + res.json(searchResults); + return; + } else { + hruidList = themes.flatMap((theme) => theme.hruids); + } + + const learningPaths = await learningPathService.fetchLearningPaths(hruidList, language as Language, `HRUIDs: ${hruidList.join(', ')}`, forGroup); + res.json(learningPaths.data); + } +} + +function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: Response) => Promise { + return async (req, res) => { + const path: LearningPath = req.body; + if (isPut) { + if (req.params.hruid !== path.hruid || req.params.language !== path.language) { + throw new BadRequestException("id_not_matching_query_params"); + } + } + const teacher = await getTeacher(req.auth!.username); + res.json(await learningPathService.createNewLearningPath(path, [teacher], isPut)); + } +} + +export const postLearningPath = postOrPutLearningPath(false); +export const putLearningPath = postOrPutLearningPath(true); + +export async function deleteLearningPath(req: AuthenticatedRequest, res: Response): Promise { + const id: LearningPathIdentifier = { + hruid: req.params.hruid, + language: req.params.language as Language + }; + const deletedPath = await learningPathService.deleteLearningPath(id); + if (deletedPath) { + res.json(deletedPath); + } else { + throw new NotFoundException("The learning path could not be found."); + } } diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index 67f08a03..beb0abec 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -28,6 +28,21 @@ export class LearningPathRepository extends DwengoEntityRepository }); } + /** + * Returns all learning paths which have the user with the given username as an administrator. + */ + public async findAllByAdminUsername(adminUsername: string): Promise { + return this.findAll({ + where: { + admins: { + $contains: { + username: adminUsername + } + } + } + }); + } + public createNode(nodeData: RequiredEntityData): LearningPathNode { return this.em.create(LearningPathNode, nodeData); } @@ -50,4 +65,16 @@ export class LearningPathRepository extends DwengoEntityRepository await Promise.all(nodes.map(async (it) => em.persistAndFlush(it))); await Promise.all(transitions.map(async (it) => em.persistAndFlush(it))); } + + /** + * Deletes the learning path with the given hruid and language. + * @returns the deleted learning path or null if it was not found. + */ + public async deleteByHruidAndLanguage(hruid: string, language: Language): Promise { + const path = await this.findByHruidAndLanguage(hruid, language); + if (path) { + await this.em.removeAndFlush(path); + } + return path; + } } diff --git a/backend/src/middleware/auth/checks/learning-path-auth-checks.ts b/backend/src/middleware/auth/checks/learning-path-auth-checks.ts new file mode 100644 index 00000000..6c73e22e --- /dev/null +++ b/backend/src/middleware/auth/checks/learning-path-auth-checks.ts @@ -0,0 +1,12 @@ +import learningPathService from "../../../services/learning-paths/learning-path-service"; +import { authorize } from "../auth"; +import { AuthenticatedRequest } from "../authenticated-request"; +import { AuthenticationInfo } from "../authentication-info"; + +export const onlyAdminsForLearningPath = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const adminsForLearningPath = learningPathService.getAdmins({ + hruid: req.body.hruid, + language: req.body.language + }); + return adminsForLearningPath && auth.username in adminsForLearningPath; +}); diff --git a/backend/src/routes/learning-paths.ts b/backend/src/routes/learning-paths.ts index efe17312..b2e67d57 100644 --- a/backend/src/routes/learning-paths.ts +++ b/backend/src/routes/learning-paths.ts @@ -1,5 +1,7 @@ import express from 'express'; -import { getLearningPaths } from '../controllers/learning-paths.js'; +import { deleteLearningPath, getLearningPaths, postLearningPath, putLearningPath } from '../controllers/learning-paths.js'; +import { teachersOnly } from '../middleware/auth/auth.js'; +import { onlyAdminsForLearningObject } from '../middleware/auth/checks/learning-object-auth-checks.js'; const router = express.Router(); @@ -23,5 +25,9 @@ const router = express.Router(); // Example: http://localhost:3000/learningPath?theme=kiks router.get('/', getLearningPaths); +router.post('/', teachersOnly, postLearningPath) + +router.put('/:hruid/:language', onlyAdminsForLearningObject, putLearningPath); +router.delete('/:hruid/:language', onlyAdminsForLearningObject, deleteLearningPath); export default router; diff --git a/backend/src/services/learning-paths/database-learning-path-provider.ts b/backend/src/services/learning-paths/database-learning-path-provider.ts index fe05dda1..ac525831 100644 --- a/backend/src/services/learning-paths/database-learning-path-provider.ts +++ b/backend/src/services/learning-paths/database-learning-path-provider.ts @@ -198,6 +198,15 @@ const databaseLearningPathProvider: LearningPathProvider = { }; }, + /** + * Returns all the learning paths which have the user with the given username as an administrator. + */ + async getLearningPathsAdministratedBy(adminUsername: string): Promise { + const repo = getLearningPathRepository(); + const paths = await repo.findAllByAdminUsername(adminUsername); + return await Promise.all(paths.map(async (result, index) => convertLearningPath(result, index))); + }, + /** * Search learning paths in the database using the given search string. */ diff --git a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts index 110cd570..f379c049 100644 --- a/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts +++ b/backend/src/services/learning-paths/dwengo-api-learning-path-provider.ts @@ -45,6 +45,10 @@ const dwengoApiLearningPathProvider: LearningPathProvider = { const searchResults = await fetchWithLogging(apiUrl, `Search learning paths with query "${query}"`, { params }); return searchResults ?? []; }, + + async getLearningPathsAdministratedBy(_adminUsername: string) { + return []; // Learning paths fetched from the Dwengo API cannot be administrated by a user. + }, }; export default dwengoApiLearningPathProvider; diff --git a/backend/src/services/learning-paths/learning-path-provider.ts b/backend/src/services/learning-paths/learning-path-provider.ts index 086777bd..0cf507ca 100644 --- a/backend/src/services/learning-paths/learning-path-provider.ts +++ b/backend/src/services/learning-paths/learning-path-provider.ts @@ -15,4 +15,9 @@ export interface LearningPathProvider { * Search learning paths in the data source using the given search string. */ searchLearningPaths(query: string, language: Language, personalizedFor?: Group): Promise; + + /** + * Get all learning paths which have the teacher with the given user as an administrator. + */ + getLearningPathsAdministratedBy(adminUsername: string): Promise; } diff --git a/backend/src/services/learning-paths/learning-path-service.ts b/backend/src/services/learning-paths/learning-path-service.ts index b20d8f97..53c084fd 100644 --- a/backend/src/services/learning-paths/learning-path-service.ts +++ b/backend/src/services/learning-paths/learning-path-service.ts @@ -1,7 +1,7 @@ import dwengoApiLearningPathProvider from './dwengo-api-learning-path-provider.js'; import databaseLearningPathProvider from './database-learning-path-provider.js'; import { envVars, getEnvVar } from '../../util/envVars.js'; -import { LearningObjectNode, LearningPath, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; +import { LearningObjectNode, LearningPath, LearningPathIdentifier, LearningPathResponse } from '@dwengo-1/common/interfaces/learning-content'; import { Language } from '@dwengo-1/common/util/language'; import { Group } from '../../entities/assignments/group.entity.js'; import { LearningPath as LearningPathEntity } from '../../entities/content/learning-path.entity.js'; @@ -12,6 +12,7 @@ import { base64ToArrayBuffer } from '../../util/base64-buffer-conversion.js'; import { TeacherDTO } from '@dwengo-1/common/interfaces/teacher'; import { mapToTeacher } from '../../interfaces/teacher.js'; import { Collection } from '@mikro-orm/core'; +import { NotFoundException } from '../../exceptions/not-found-exception.js'; const userContentPrefix = getEnvVar(envVars.UserContentPrefix); const allProviders = [dwengoApiLearningPathProvider, databaseLearningPathProvider]; @@ -105,6 +106,16 @@ const learningPathService = { }; }, + /** + * Fetch the learning paths administrated by the teacher with the given username. + */ + async getLearningPathsAdministratedBy(adminUsername: string): Promise { + const providerResponses = await Promise.all( + allProviders.map(async (provider) => provider.getLearningPathsAdministratedBy(adminUsername)) + ); + return providerResponses.flat(); + }, + /** * Search learning paths in the data source using the given search string. */ @@ -119,12 +130,42 @@ const learningPathService = { * Add a new learning path to the database. * @param dto Learning path DTO from which the learning path will be created. * @param admins Teachers who should become an admin of the learning path. + * @param allowReplace If this is set to true and there is already a learning path with the same identifier, it is replaced. + * @returns the created learning path. */ - async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[]): Promise { + async createNewLearningPath(dto: LearningPath, admins: TeacherDTO[], allowReplace = false): Promise { const repo = getLearningPathRepository(); const path = mapToLearningPath(dto, admins); - await repo.save(path, { preventOverwrite: true }); + await repo.save(path, { preventOverwrite: allowReplace }); + return path; }, + + /** + * Deletes the learning path with the given identifier from the database. + * @param id Identifier of the learning path to delete. + * @returns the deleted learning path. + */ + async deleteLearningPath(id: LearningPathIdentifier): Promise { + const repo = getLearningPathRepository(); + const deletedPath = await repo.deleteByHruidAndLanguage(id.hruid, id.language); + if (deletedPath) { + return deletedPath; + } + throw new NotFoundException("No learning path with the given identifier found."); + }, + + /** + * Returns a list of the usernames of the administrators of the learning path with the given identifier. + * @param id The identifier of the learning path whose admins should be fetched. + */ + async getAdmins(id: LearningPathIdentifier): Promise { + const repo = getLearningPathRepository(); + const path = await repo.findByHruidAndLanguage(id.hruid, id.language); + if (!path) { + throw new NotFoundException("No learning path with the given identifier found."); + } + return path.admins.map(admin => admin.username); + } }; export default learningPathService; From a6e0c4bbd67d2dc10d33560d6eb5f25a3d756c81 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 12 May 2025 21:45:31 +0200 Subject: [PATCH 036/117] feat(frontend): Frontend-controllers voor het beheren van leerpaden & verwijderen van leerobjecten aangemaakt --- frontend/src/controllers/learning-objects.ts | 4 ++++ frontend/src/controllers/learning-paths.ts | 17 +++++++++++++++++ 2 files changed, 21 insertions(+) diff --git a/frontend/src/controllers/learning-objects.ts b/frontend/src/controllers/learning-objects.ts index 7239b88f..a9ecf22f 100644 --- a/frontend/src/controllers/learning-objects.ts +++ b/frontend/src/controllers/learning-objects.ts @@ -22,4 +22,8 @@ export class LearningObjectController extends BaseController { async upload(learningObjectZip: File): Promise { return this.postFile("/", "learningObject", learningObjectZip); } + + async deleteLearningObject(hruid: string, language: Language, version: number): Promise { + return this.delete(`/${hruid}`, { language, version }); + } } diff --git a/frontend/src/controllers/learning-paths.ts b/frontend/src/controllers/learning-paths.ts index bad54286..697826bc 100644 --- a/frontend/src/controllers/learning-paths.ts +++ b/frontend/src/controllers/learning-paths.ts @@ -36,4 +36,21 @@ export class LearningPathController extends BaseController { const dtos = await this.get("/", query); return dtos.map((dto) => LearningPath.fromDTO(dto)); } + + async getAllByAdmin(admin: string): Promise { + const dtos = await this.get("/", { admin }); + return dtos.map((dto) => LearningPath.fromDTO(dto)); + } + + async postLearningPath(learningPath: LearningPathDTO): Promise { + return await this.post("/", learningPath); + } + + async putLearningPath(learningPath: LearningPathDTO): Promise { + return await this.put(`/${learningPath.hruid}/${learningPath.language}`, learningPath); + } + + async deleteLearningPath(hruid: string, language: string): Promise { + return await this.delete(`/${hruid}/${language}`); + } } From 69292885545527ea3ed4a0339ff11ab49637f376 Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Mon, 12 May 2025 21:51:26 +0200 Subject: [PATCH 037/117] feat(frontend): LearningObjectDeletionMutation toegevoegd --- frontend/src/queries/learning-objects.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/frontend/src/queries/learning-objects.ts b/frontend/src/queries/learning-objects.ts index 5e7612e9..e06f21c5 100644 --- a/frontend/src/queries/learning-objects.ts +++ b/frontend/src/queries/learning-objects.ts @@ -5,6 +5,7 @@ import { getLearningObjectController } from "@/controllers/controllers.ts"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; import type { AxiosError } from "axios"; +import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; export const LEARNING_OBJECT_KEY = "learningObject"; const learningObjectController = getLearningObjectController(); @@ -78,3 +79,12 @@ export function useUploadLearningObjectMutation(): UseMutationReturnType { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } }); } + +export function useDeleteLearningObjectMutation(): UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ hruid, language, version }) => await learningObjectController.deleteLearningObject(hruid, language, version), + onSuccess: async () => { await queryClient.invalidateQueries({queryKey: [LEARNING_OBJECT_KEY, "forAdmin"]}); } + }); +} From 1a768fedccaa0ac0121e3fc6e5d62823fc1b471e Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Tue, 13 May 2025 01:02:53 +0200 Subject: [PATCH 038/117] fix(backend): Bugs omtrent leerpad-endpoints opgelost --- backend/src/data/content/learning-object-repository.ts | 2 +- backend/src/data/content/learning-path-repository.ts | 4 +--- backend/src/entities/content/learning-object.entity.ts | 2 ++ .../middleware/auth/checks/learning-object-auth-checks.ts | 2 +- .../middleware/auth/checks/learning-path-auth-checks.ts | 6 +++--- .../learning-object-zip-processing-service.ts | 8 ++++---- 6 files changed, 12 insertions(+), 12 deletions(-) diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 889370d5..a862bfc2 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -12,7 +12,7 @@ export class LearningObjectRepository extends DwengoEntityRepository return this.findAll({ where: { admins: { - $contains: { - username: adminUsername - } + username: adminUsername } } }); diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index 825bf744..59593c9a 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,5 +1,6 @@ import { ArrayType, + Cascade, Collection, Embedded, Entity, @@ -93,6 +94,7 @@ export class LearningObject { @OneToMany({ entity: () => Attachment, mappedBy: 'learningObject', + cascade: [Cascade.ALL] }) attachments: Collection = new Collection(this); diff --git a/backend/src/middleware/auth/checks/learning-object-auth-checks.ts b/backend/src/middleware/auth/checks/learning-object-auth-checks.ts index 31387198..7ef91947 100644 --- a/backend/src/middleware/auth/checks/learning-object-auth-checks.ts +++ b/backend/src/middleware/auth/checks/learning-object-auth-checks.ts @@ -12,5 +12,5 @@ export const onlyAdminsForLearningObject = authorize(async (auth: Authentication language: language as Language, version: parseInt(version as string) }); - return auth.username in admins; + return admins.includes(auth.username); }); diff --git a/backend/src/middleware/auth/checks/learning-path-auth-checks.ts b/backend/src/middleware/auth/checks/learning-path-auth-checks.ts index 6c73e22e..64e51416 100644 --- a/backend/src/middleware/auth/checks/learning-path-auth-checks.ts +++ b/backend/src/middleware/auth/checks/learning-path-auth-checks.ts @@ -3,10 +3,10 @@ import { authorize } from "../auth"; import { AuthenticatedRequest } from "../authenticated-request"; import { AuthenticationInfo } from "../authentication-info"; -export const onlyAdminsForLearningPath = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => { - const adminsForLearningPath = learningPathService.getAdmins({ +export const onlyAdminsForLearningPath = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { + const adminsForLearningPath = await learningPathService.getAdmins({ hruid: req.body.hruid, language: req.body.language }); - return adminsForLearningPath && auth.username in adminsForLearningPath; + return adminsForLearningPath && adminsForLearningPath.includes(auth.username); }); diff --git a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts index 213c3f17..a502aecd 100644 --- a/backend/src/services/learning-objects/learning-object-zip-processing-service.ts +++ b/backend/src/services/learning-objects/learning-object-zip-processing-service.ts @@ -18,7 +18,7 @@ export async function processLearningObjectZip(filePath: string): Promise Date: Tue, 13 May 2025 01:03:55 +0200 Subject: [PATCH 039/117] feat(frontend): Heel ruwe eerste versie van leerpadbeheerpagina toegevoegd --- frontend/package.json | 1 + frontend/src/components/MenuBar.vue | 3 + frontend/src/controllers/learning-paths.ts | 5 +- frontend/src/i18n/locale/en.json | 15 +- frontend/src/main.ts | 10 + frontend/src/queries/learning-objects.ts | 1 - frontend/src/queries/learning-paths.ts | 44 +- .../OwnLearningContentPage.vue | 20 +- .../OwnLearningPathsView.vue | 9 - .../LearningObjectPreviewCard.vue | 50 + .../LearningObjectUploadButton.vue | 9 +- .../OwnLearningObjectsView.vue | 29 +- .../LearningPathPreviewCard.vue | 62 + .../learning-paths/OwnLearningPathsView.vue | 60 + package-lock.json | 6234 ++--------------- 15 files changed, 732 insertions(+), 5820 deletions(-) delete mode 100644 frontend/src/views/own-learning-content/OwnLearningPathsView.vue create mode 100644 frontend/src/views/own-learning-content/learning-objects/LearningObjectPreviewCard.vue rename frontend/src/views/own-learning-content/{ => learning-objects}/LearningObjectUploadButton.vue (85%) rename frontend/src/views/own-learning-content/{ => learning-objects}/OwnLearningObjectsView.vue (59%) create mode 100644 frontend/src/views/own-learning-content/learning-paths/LearningPathPreviewCard.vue create mode 100644 frontend/src/views/own-learning-content/learning-paths/OwnLearningPathsView.vue diff --git a/frontend/package.json b/frontend/package.json index 0826edae..58287e14 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -21,6 +21,7 @@ "@tanstack/vue-query": "^5.69.0", "@vueuse/core": "^13.1.0", "axios": "^1.8.2", + "json-editor-vue": "^0.18.1", "oidc-client-ts": "^3.1.0", "rollup": "^4.40.0", "uuid": "^11.1.0", diff --git a/frontend/src/components/MenuBar.vue b/frontend/src/components/MenuBar.vue index a58be2f8..d8eccacb 100644 --- a/frontend/src/components/MenuBar.vue +++ b/frontend/src/components/MenuBar.vue @@ -7,8 +7,10 @@ // Import assets import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; +import { useLocale } from "vuetify"; const { t, locale } = useI18n(); + const { current: vuetifyLocale } = useLocale(); const role = auth.authState.activeRole; const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable @@ -31,6 +33,7 @@ // Logic to change the language of the website to the selected language function changeLanguage(langCode: string): void { locale.value = langCode; + vuetifyLocale.value = langCode; localStorage.setItem("user-lang", langCode); } diff --git a/frontend/src/controllers/learning-paths.ts b/frontend/src/controllers/learning-paths.ts index 697826bc..f8b82bef 100644 --- a/frontend/src/controllers/learning-paths.ts +++ b/frontend/src/controllers/learning-paths.ts @@ -37,9 +37,8 @@ export class LearningPathController extends BaseController { return dtos.map((dto) => LearningPath.fromDTO(dto)); } - async getAllByAdmin(admin: string): Promise { - const dtos = await this.get("/", { admin }); - return dtos.map((dto) => LearningPath.fromDTO(dto)); + async getAllByAdminRaw(admin: string): Promise { + return await this.get("/", { admin }); } async postLearningPath(learningPath: LearningPathDTO): Promise { diff --git a/frontend/src/i18n/locale/en.json b/frontend/src/i18n/locale/en.json index e4042d09..47acd255 100644 --- a/frontend/src/i18n/locale/en.json +++ b/frontend/src/i18n/locale/en.json @@ -121,5 +121,18 @@ "invite": "invite", "assignmentIndicator": "ASSIGNMENT", "searchAllLearningPathsTitle": "Search all learning paths", - "searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths." + "searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.", + "learningObjects": "Learning objects", + "learningPaths": "Learning paths", + "hruid": "HRUID", + "language": "Language", + "version": "Version", + "previewFor": "Preview for ", + "upload": "Upload", + "learningObjectUploadTitle": "Upload a learning object", + "uploadFailed": "Upload failed", + "invalidZip": "This is not a valid zip file.", + "emptyZip": "This zip file is empty", + "missingMetadata": "This learning object is missing a metadata.json file.", + "missingContent": "This learning object is missing a content.* file." } diff --git a/frontend/src/main.ts b/frontend/src/main.ts index b5315634..f6f90716 100644 --- a/frontend/src/main.ts +++ b/frontend/src/main.ts @@ -7,15 +7,20 @@ import * as components from "vuetify/components"; import * as directives from "vuetify/directives"; import i18n from "./i18n/i18n.ts"; +// JSON-editor +import JsonEditorVue from 'json-editor-vue'; + // Components import App from "./App.vue"; import router from "./router"; import { aliases, mdi } from "vuetify/iconsets/mdi"; import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query"; +import { de, en, fr, nl } from "vuetify/locale"; const app = createApp(App); app.use(router); +app.use(JsonEditorVue, {}) const link = document.createElement("link"); link.rel = "stylesheet"; @@ -32,6 +37,11 @@ const vuetify = createVuetify({ mdi, }, }, + locale: { + locale: i18n.global.locale, + fallback: 'en', + messages: { nl, en, de, fr } + } }); const queryClient = new QueryClient({ diff --git a/frontend/src/queries/learning-objects.ts b/frontend/src/queries/learning-objects.ts index e06f21c5..2ed8bc52 100644 --- a/frontend/src/queries/learning-objects.ts +++ b/frontend/src/queries/learning-objects.ts @@ -5,7 +5,6 @@ import { getLearningObjectController } from "@/controllers/controllers.ts"; import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; import type { AxiosError } from "axios"; -import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content"; export const LEARNING_OBJECT_KEY = "learningObject"; const learningObjectController = getLearningObjectController(); diff --git a/frontend/src/queries/learning-paths.ts b/frontend/src/queries/learning-paths.ts index 1f088c9d..d24b6fc5 100644 --- a/frontend/src/queries/learning-paths.ts +++ b/frontend/src/queries/learning-paths.ts @@ -1,8 +1,10 @@ import { type MaybeRefOrGetter, toValue } from "vue"; import type { Language } from "@/data-objects/language.ts"; -import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query"; +import { useMutation, useQuery, useQueryClient, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query"; import { getLearningPathController } from "@/controllers/controllers"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; +import type { AxiosError } from "axios"; +import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto"; export const LEARNING_PATH_KEY = "learningPath"; const learningPathController = getLearningPathController(); @@ -32,6 +34,46 @@ export function useGetAllLearningPathsByThemeQuery( }); } +export function useGetAllLearningPathsByAdminQuery( + admin: MaybeRefOrGetter +): UseQueryReturnType { + return useQuery({ + queryKey: [LEARNING_PATH_KEY, "getAllByAdmin", admin], + queryFn: async () => learningPathController.getAllByAdminRaw(toValue(admin)!), + enabled: () => Boolean(toValue(admin)) + }); +} + +export function usePostLearningPathMutation(): + UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ learningPath }) => learningPathController.postLearningPath(learningPath), + onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }) + }); +} + +export function usePutLearningPathMutation(): + UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ learningPath }) => learningPathController.putLearningPath(learningPath), + onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }) + }); +} + +export function useDeleteLearningPathMutation(): + UseMutationReturnType { + const queryClient = useQueryClient(); + + return useMutation({ + mutationFn: async ({ hruid, language }) => learningPathController.deleteLearningPath(hruid, language), + onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }) + }); +} + export function useSearchLearningPathQuery( query: MaybeRefOrGetter, language: MaybeRefOrGetter, diff --git a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue index 609c8433..02a2e90d 100644 --- a/frontend/src/views/own-learning-content/OwnLearningContentPage.vue +++ b/frontend/src/views/own-learning-content/OwnLearningContentPage.vue @@ -1,18 +1,23 @@ @@ -34,7 +39,12 @@ import { useI18n } from "vue-i18n"; - + + + @@ -45,8 +55,10 @@ import { useI18n } from "vue-i18n"; display: flex; flex-direction: column; height: 100%; + padding: 20px 30px; } .main-content { - flex: 1; + flex: 1 1; + height: 100%; } diff --git a/frontend/src/views/own-learning-content/OwnLearningPathsView.vue b/frontend/src/views/own-learning-content/OwnLearningPathsView.vue deleted file mode 100644 index b199202e..00000000 --- a/frontend/src/views/own-learning-content/OwnLearningPathsView.vue +++ /dev/null @@ -1,9 +0,0 @@ - - - - - diff --git a/frontend/src/views/own-learning-content/learning-objects/LearningObjectPreviewCard.vue b/frontend/src/views/own-learning-content/learning-objects/LearningObjectPreviewCard.vue new file mode 100644 index 00000000..3f475863 --- /dev/null +++ b/frontend/src/views/own-learning-content/learning-objects/LearningObjectPreviewCard.vue @@ -0,0 +1,50 @@ + + + + + diff --git a/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue b/frontend/src/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue similarity index 85% rename from frontend/src/views/own-learning-content/LearningObjectUploadButton.vue rename to frontend/src/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue index 34211467..4507401b 100644 --- a/frontend/src/views/own-learning-content/LearningObjectUploadButton.vue +++ b/frontend/src/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue @@ -32,17 +32,14 @@ - + diff --git a/frontend/src/components/LearningPathsGrid.vue b/frontend/src/components/LearningPathsGrid.vue index 865c7166..8df08a00 100644 --- a/frontend/src/components/LearningPathsGrid.vue +++ b/frontend/src/components/LearningPathsGrid.vue @@ -53,9 +53,9 @@ white-space: normal; } .results-grid { - margin: 20px; + margin: 20px auto; display: flex; - align-items: stretch; + justify-content: center; gap: 20px; flex-wrap: wrap; } diff --git a/frontend/src/views/SingleTheme.vue b/frontend/src/views/SingleTheme.vue index 6924cc1c..4b14d606 100644 --- a/frontend/src/views/SingleTheme.vue +++ b/frontend/src/views/SingleTheme.vue @@ -31,13 +31,14 @@ From e4884686503d5b9b4d502588a954f3ea6867146a Mon Sep 17 00:00:00 2001 From: Gerald Schmittinger Date: Thu, 15 May 2025 20:46:46 +0200 Subject: [PATCH 081/117] fix(backend): Fout bij het gebruiken van requireFields opgelost. --- backend/src/controllers/learning-paths.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 35a40785..c342306f 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -76,7 +76,9 @@ function postOrPutLearningPath(isPut: boolean): (req: AuthenticatedRequest, res: const path: LearningPath = req.body; const { hruid: hruidParam, language: languageParam } = req.params; - requireFields({ hruidParam, languageParam, path }); + if (isPut) { + requireFields({ hruidParam, languageParam, path }); + } const teacher = await getTeacher(req.auth!.username); if (isPut) { From 26a01f0f30750d801dddb1b22e46dc8c389f9cd4 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Thu, 15 May 2025 20:52:47 +0200 Subject: [PATCH 082/117] refactor: prevent impersonation middelware --- .../middleware/auth/checks/user-auth-checks.ts | 2 +- backend/src/routes/student-join-requests.ts | 6 +++--- backend/src/routes/students.ts | 16 ++++++++-------- backend/src/routes/teacher-invitations.ts | 4 ++-- backend/src/routes/teachers.ts | 12 ++++++------ 5 files changed, 20 insertions(+), 20 deletions(-) diff --git a/backend/src/middleware/auth/checks/user-auth-checks.ts b/backend/src/middleware/auth/checks/user-auth-checks.ts index f66f6682..27228369 100644 --- a/backend/src/middleware/auth/checks/user-auth-checks.ts +++ b/backend/src/middleware/auth/checks/user-auth-checks.ts @@ -5,4 +5,4 @@ import { AuthenticatedRequest } from '../authenticated-request.js'; /** * Only allow the user whose username is in the path parameter "username" to access the endpoint. */ -export const onlyAllowUserHimself = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username); +export const preventImpersonation = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username); diff --git a/backend/src/routes/student-join-requests.ts b/backend/src/routes/student-join-requests.ts index 35198d0c..a49984c7 100644 --- a/backend/src/routes/student-join-requests.ts +++ b/backend/src/routes/student-join-requests.ts @@ -5,16 +5,16 @@ import { getStudentRequestHandler, getStudentRequestsHandler, } from '../controllers/students.js'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; import { onlyAllowStudentHimselfAndTeachersOfClass } from '../middleware/auth/checks/class-auth-checks.js'; // Under /:username/joinRequests/ const router = express.Router({ mergeParams: true }); -router.get('/', onlyAllowUserHimself, getStudentRequestsHandler); +router.get('/', preventImpersonation, getStudentRequestsHandler); -router.post('/', onlyAllowUserHimself, createStudentRequestHandler); +router.post('/', preventImpersonation, createStudentRequestHandler); router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler); diff --git a/backend/src/routes/students.ts b/backend/src/routes/students.ts index f40ce939..9ecf4688 100644 --- a/backend/src/routes/students.ts +++ b/backend/src/routes/students.ts @@ -11,7 +11,7 @@ import { getStudentSubmissionsHandler, } from '../controllers/students.js'; import joinRequestRouter from './student-join-requests.js'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; import { adminOnly } from '../middleware/auth/checks/auth-checks.js'; const router = express.Router(); @@ -23,25 +23,25 @@ router.get('/', adminOnly, getAllStudentsHandler); // Can only be used by an administrator. router.post('/', adminOnly, createStudentHandler); -router.delete('/:username', onlyAllowUserHimself, deleteStudentHandler); +router.delete('/:username', preventImpersonation, deleteStudentHandler); // Information about a student's profile -router.get('/:username', onlyAllowUserHimself, getStudentHandler); +router.get('/:username', preventImpersonation, getStudentHandler); // The list of classes a student is in -router.get('/:username/classes', onlyAllowUserHimself, getStudentClassesHandler); +router.get('/:username/classes', preventImpersonation, getStudentClassesHandler); // The list of submissions a student has made -router.get('/:username/submissions', onlyAllowUserHimself, getStudentSubmissionsHandler); +router.get('/:username/submissions', preventImpersonation, getStudentSubmissionsHandler); // The list of assignments a student has -router.get('/:username/assignments', onlyAllowUserHimself, getStudentAssignmentsHandler); +router.get('/:username/assignments', preventImpersonation, getStudentAssignmentsHandler); // The list of groups a student is in -router.get('/:username/groups', onlyAllowUserHimself, getStudentGroupsHandler); +router.get('/:username/groups', preventImpersonation, getStudentGroupsHandler); // A list of questions a user has created -router.get('/:username/questions', onlyAllowUserHimself, getStudentQuestionsHandler); +router.get('/:username/questions', preventImpersonation, getStudentQuestionsHandler); router.use('/:username/joinRequests', joinRequestRouter); diff --git a/backend/src/routes/teacher-invitations.ts b/backend/src/routes/teacher-invitations.ts index 0855c6a6..90117088 100644 --- a/backend/src/routes/teacher-invitations.ts +++ b/backend/src/routes/teacher-invitations.ts @@ -6,7 +6,7 @@ import { getInvitationHandler, updateInvitationHandler, } from '../controllers/teacher-invitations.js'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; import { onlyAllowReceiverBody, onlyAllowSender, @@ -16,7 +16,7 @@ import { const router = express.Router({ mergeParams: true }); -router.get('/:username', onlyAllowUserHimself, getAllInvitationsHandler); +router.get('/:username', preventImpersonation, getAllInvitationsHandler); router.get('/:sender/:receiver/:classId', onlyAllowSenderOrReceiver, getInvitationHandler); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index 26ec77be..9c12ad13 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -12,7 +12,7 @@ import { } from '../controllers/teachers.js'; import invitationRouter from './teacher-invitations.js'; import { adminOnly } from '../middleware/auth/checks/auth-checks.js'; -import { onlyAllowUserHimself } from '../middleware/auth/checks/user-auth-checks.js'; +import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js'; import { onlyAllowTeacherOfClass } from '../middleware/auth/checks/class-auth-checks.js'; const router = express.Router(); @@ -21,15 +21,15 @@ router.get('/', adminOnly, getAllTeachersHandler); router.post('/', adminOnly, createTeacherHandler); -router.get('/:username', onlyAllowUserHimself, getTeacherHandler); +router.get('/:username', preventImpersonation, getTeacherHandler); -router.delete('/:username', onlyAllowUserHimself, deleteTeacherHandler); +router.delete('/:username', preventImpersonation, deleteTeacherHandler); -router.get('/:username/classes', onlyAllowUserHimself, getTeacherClassHandler); +router.get('/:username/classes', preventImpersonation, getTeacherClassHandler); -router.get('/:username/students', onlyAllowUserHimself, getTeacherStudentHandler); +router.get('/:username/students', preventImpersonation, getTeacherStudentHandler); -router.get('/:username/questions', onlyAllowUserHimself, getTeacherQuestionHandler); +router.get('/:username/questions', preventImpersonation, getTeacherQuestionHandler); router.get('/:username/joinRequests/:classId', onlyAllowTeacherOfClass, getStudentJoinRequestHandler); From f9fc19d8a66e3226a26dba7d4d35c2b05048ff77 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Thu, 15 May 2025 20:53:35 +0200 Subject: [PATCH 083/117] fix: duplicate studenten in lijst gefilterd --- backend/src/services/teachers.ts | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/backend/src/services/teachers.ts b/backend/src/services/teachers.ts index aa67f211..dd70b26c 100644 --- a/backend/src/services/teachers.ts +++ b/backend/src/services/teachers.ts @@ -112,7 +112,13 @@ export async function getStudentsByTeacher(username: string, full: boolean): Pro const classIds: string[] = classes.map((cls) => cls.id); - const students: StudentDTO[] = (await Promise.all(classIds.map(async (username) => await getClassStudentsDTO(username)))).flat(); + const students: StudentDTO[] = (await Promise.all( + classIds.map(async (classId) => await getClassStudentsDTO(classId)) + )) + .flat() + .filter((student, index, self) => + self.findIndex((s) => s.username === student.username) === index + ); if (full) { return students; From fc0d3b5c84f1dd898a25788c92be84680a9683a5 Mon Sep 17 00:00:00 2001 From: Gabriellvl Date: Thu, 15 May 2025 20:54:43 +0200 Subject: [PATCH 084/117] refactor: enum voor account types --- backend/src/controllers/auth.ts | 3 +- .../auth/checks/assignment-auth-checks.ts | 3 +- .../src/middleware/auth/checks/auth-checks.ts | 5 +- .../auth/checks/class-auth-checks.ts | 9 +- .../auth/checks/group-auth-checker.ts | 3 +- .../checks/learning-content-auth-checks.ts | 3 +- .../middleware/auth/checks/question-checks.ts | 3 +- .../auth/checks/submission-checks.ts | 3 +- common/src/util/account-types.ts | 4 + frontend/package.json | 1 + frontend/src/components/SingleQuestion.vue | 3 +- frontend/src/views/LoginPage.vue | 5 +- .../views/assignments/CreateAssignment.vue | 3 +- .../views/assignments/SingleAssignment.vue | 3 +- .../src/views/assignments/UserAssignments.vue | 3 +- frontend/src/views/classes/UserClasses.vue | 3 +- .../views/learning-paths/LearningPathPage.vue | 15 +- .../learning-object/LearningObjectView.vue | 3 +- .../submissions/SubmitButton.vue | 3 +- package-lock.json | 5869 +---------------- 20 files changed, 77 insertions(+), 5870 deletions(-) create mode 100644 common/src/util/account-types.ts diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index ca79da59..2daace17 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -5,6 +5,7 @@ import { envVars, getEnvVar } from '../util/envVars.js'; import { createOrUpdateStudent } from '../services/students.js'; import { Request, Response } from 'express'; import { createOrUpdateTeacher } from '../services/teachers.js'; +import {AccountType} from "@dwengo-1/common/util/account-types"; interface FrontendIdpConfig { authority: string; @@ -55,7 +56,7 @@ export async function postHelloHandler(req: AuthenticatedRequest, res: Response) firstName: auth.firstName ?? '', lastName: auth.lastName ?? '', }; - if (auth.accountType === 'student') { + if (auth.accountType === AccountType.Student) { await createOrUpdateStudent(userData); logger.debug(`Synchronized student ${userData.username} with IDP`); } else { diff --git a/backend/src/middleware/auth/checks/assignment-auth-checks.ts b/backend/src/middleware/auth/checks/assignment-auth-checks.ts index f8e1e3d7..c812ef43 100644 --- a/backend/src/middleware/auth/checks/assignment-auth-checks.ts +++ b/backend/src/middleware/auth/checks/assignment-auth-checks.ts @@ -2,6 +2,7 @@ import { authorize } from './auth-checks.js'; import { fetchClass } from '../../../services/classes.js'; import { fetchAllGroups } from '../../../services/groups.js'; import { mapToUsername } from '../../../interfaces/user.js'; +import {AccountType} from "@dwengo-1/common/util/account-types"; /** * Expects the path to contain the path parameters 'classId' and 'id' (meaning the ID of the assignment). @@ -11,7 +12,7 @@ import { mapToUsername } from '../../../interfaces/user.js'; */ export const onlyAllowIfHasAccessToAssignment = authorize(async (auth, req) => { const { classid: classId, id: assignmentId } = req.params as { classid: string; id: number }; - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { const clazz = await fetchClass(classId); return clazz.teachers.map(mapToUsername).includes(auth.username); } diff --git a/backend/src/middleware/auth/checks/auth-checks.ts b/backend/src/middleware/auth/checks/auth-checks.ts index 6afe92e7..9fe7903d 100644 --- a/backend/src/middleware/auth/checks/auth-checks.ts +++ b/backend/src/middleware/auth/checks/auth-checks.ts @@ -4,6 +4,7 @@ import * as express from 'express'; import { RequestHandler } from 'express'; import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js'; import { ForbiddenException } from '../../../exceptions/forbidden-exception.js'; +import {AccountType} from "@dwengo-1/common/util/account-types"; /** * Middleware which rejects unauthenticated users (with HTTP 401) and authenticated users which do not fulfill @@ -36,11 +37,11 @@ export const authenticatedOnly = authorize((_) => true); /** * Middleware which rejects requests from unauthenticated users or users that aren't students. */ -export const studentsOnly = authorize((auth) => auth.accountType === 'student'); +export const studentsOnly = authorize((auth) => auth.accountType === AccountType.Student); /** * Middleware which rejects requests from unauthenticated users or users that aren't teachers. */ -export const teachersOnly = authorize((auth) => auth.accountType === 'teacher'); +export const teachersOnly = authorize((auth) => auth.accountType === AccountType.Teacher); /** * Middleware which is to be used on requests no normal user should be able to execute. * Since there is no concept of administrator accounts yet, currently, those requests will always be blocked. diff --git a/backend/src/middleware/auth/checks/class-auth-checks.ts b/backend/src/middleware/auth/checks/class-auth-checks.ts index 6af44827..21272fec 100644 --- a/backend/src/middleware/auth/checks/class-auth-checks.ts +++ b/backend/src/middleware/auth/checks/class-auth-checks.ts @@ -4,6 +4,7 @@ import { AuthenticatedRequest } from '../authenticated-request.js'; import { fetchClass } from '../../../services/classes.js'; import { mapToUsername } from '../../../interfaces/user.js'; import {getAllInvitations} from "../../../services/teacher-invitations"; +import {AccountType} from "@dwengo-1/common/util/account-types"; async function teaches(teacherUsername: string, classId: string): Promise { const clazz = await fetchClass(classId); @@ -18,7 +19,7 @@ async function teaches(teacherUsername: string, classId: string): Promise { if (req.params.username === auth.username) { return true; - } else if (auth.accountType === 'teacher') { + } else if (auth.accountType === AccountType.Teacher) { return teaches(auth.username, req.params.classId); } return false; @@ -39,7 +40,7 @@ export const onlyAllowTeacherOfClass = authorize( export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const classId = req.params.classId ?? req.params.classid ?? req.params.id; const clazz = await fetchClass(classId); - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { return clazz.teachers.map(mapToUsername).includes(auth.username); } return clazz.students.map(mapToUsername).includes(auth.username); @@ -48,7 +49,7 @@ export const onlyAllowIfInClass = authorize(async (auth: AuthenticationInfo, req export const onlyAllowIfInClassOrInvited = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const classId = req.params.classId ?? req.params.classid ?? req.params.id; const clazz = await fetchClass(classId); - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { const invitations = await getAllInvitations(auth.username, false); return clazz.teachers.map(mapToUsername).includes(auth.username) || invitations.some(invitation => invitation.classId === classId); } @@ -62,7 +63,7 @@ export const onlyAllowOwnClassInBody = authorize(async (auth, req) => { const classId = (req.body as { class: string })?.class; const clazz = await fetchClass(classId); - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { return clazz.teachers.map(mapToUsername).includes(auth.username); } return clazz.students.map(mapToUsername).includes(auth.username); diff --git a/backend/src/middleware/auth/checks/group-auth-checker.ts b/backend/src/middleware/auth/checks/group-auth-checker.ts index 02230b13..408dee7d 100644 --- a/backend/src/middleware/auth/checks/group-auth-checker.ts +++ b/backend/src/middleware/auth/checks/group-auth-checker.ts @@ -2,6 +2,7 @@ import { authorize } from './auth-checks.js'; import { fetchClass } from '../../../services/classes.js'; import { fetchGroup } from '../../../services/groups.js'; import { mapToUsername } from '../../../interfaces/user.js'; +import {AccountType} from "@dwengo-1/common/util/account-types"; /** * Expects the path to contain the path parameters 'classid', 'assignmentid' and 'groupid'. @@ -16,7 +17,7 @@ export const onlyAllowIfHasAccessToGroup = authorize(async (auth, req) => { groupid: groupId, } = req.params as { classid: string; assignmentid: number; groupid: number }; - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { const clazz = await fetchClass(classId); return clazz.teachers.map(mapToUsername).includes(auth.username); } // User is student diff --git a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts index c0b34c52..fde71181 100644 --- a/backend/src/middleware/auth/checks/learning-content-auth-checks.ts +++ b/backend/src/middleware/auth/checks/learning-content-auth-checks.ts @@ -1,6 +1,7 @@ import { authorize } from './auth-checks'; import { AuthenticationInfo } from '../authentication-info'; import { AuthenticatedRequest } from '../authenticated-request'; +import {AccountType} from "@dwengo-1/common/util/account-types"; /** * Only allows requests whose learning path personalization query parameters ('forGroup' / 'assignmentNo' / 'classId') @@ -11,7 +12,7 @@ import { AuthenticatedRequest } from '../authenticated-request'; */ export const onlyAllowPersonalizationForOwnGroup = authorize(async (auth: AuthenticationInfo, req: AuthenticatedRequest) => { const { forGroup, assignmentNo, classId } = req.params; - if (auth.accountType === 'student' && forGroup && assignmentNo && classId) { + if (auth.accountType === AccountType.Student && forGroup && assignmentNo && classId) { // TODO: groupNumber? // Const group = await fetchGroup(Number(classId), Number(assignmentNo), ) return false; diff --git a/backend/src/middleware/auth/checks/question-checks.ts b/backend/src/middleware/auth/checks/question-checks.ts index 374a2aa0..fa306f8c 100644 --- a/backend/src/middleware/auth/checks/question-checks.ts +++ b/backend/src/middleware/auth/checks/question-checks.ts @@ -7,6 +7,7 @@ import { fetchQuestion } from '../../../services/questions.js'; import { FALLBACK_SEQ_NUM } from '../../../config.js'; import { fetchAnswer } from '../../../services/answers.js'; import { mapToUsername } from '../../../interfaces/user.js'; +import {AccountType} from "@dwengo-1/common/util/account-types"; export const onlyAllowAuthor = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { author: string }).author === auth.username @@ -57,7 +58,7 @@ export const onlyAllowIfHasAccessToQuestion = authorize(async (auth: Authenticat const question = await fetchQuestion(questionId); const group = question.inGroup; - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { const cls = group.assignment.within; // TODO check if contains full objects return cls.teachers.map(mapToUsername).includes(auth.username); } // User is student diff --git a/backend/src/middleware/auth/checks/submission-checks.ts b/backend/src/middleware/auth/checks/submission-checks.ts index cb84f438..df7282f6 100644 --- a/backend/src/middleware/auth/checks/submission-checks.ts +++ b/backend/src/middleware/auth/checks/submission-checks.ts @@ -6,6 +6,7 @@ import { AuthenticationInfo } from '../authentication-info.js'; import { authorize } from './auth-checks.js'; import { FALLBACK_LANG } from '../../../config.js'; import { mapToUsername } from '../../../interfaces/user.js'; +import {AccountType} from "@dwengo-1/common/util/account-types"; export const onlyAllowSubmitter = authorize( (auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { submitter: string }).submitter === auth.username @@ -18,7 +19,7 @@ export const onlyAllowIfHasAccessToSubmission = authorize(async (auth: Authentic const loId = new LearningObjectIdentifier(lohruid, languageMap[lang as string] ?? FALLBACK_LANG, Number(version)); const submission = await fetchSubmission(loId, Number(submissionNumber)); - if (auth.accountType === 'teacher') { + if (auth.accountType === AccountType.Teacher) { // Dit kan niet werken om dat al deze objecten niet gepopulate zijn. return submission.onBehalfOf.assignment.within.teachers.map(mapToUsername).includes(auth.username); } diff --git a/common/src/util/account-types.ts b/common/src/util/account-types.ts new file mode 100644 index 00000000..61c761dc --- /dev/null +++ b/common/src/util/account-types.ts @@ -0,0 +1,4 @@ +export enum AccountType { + Student = 'student', + Teacher = 'teacher' +} diff --git a/frontend/package.json b/frontend/package.json index 0826edae..86232620 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "test:e2e": "playwright test" }, "dependencies": { + "@dwengo-1/common": "^0.2.0", "@tanstack/react-query": "^5.69.0", "@tanstack/vue-query": "^5.69.0", "@vueuse/core": "^13.1.0", diff --git a/frontend/src/components/SingleQuestion.vue b/frontend/src/components/SingleQuestion.vue index 8d53b6e1..51ee3469 100644 --- a/frontend/src/components/SingleQuestion.vue +++ b/frontend/src/components/SingleQuestion.vue @@ -6,6 +6,7 @@ import type { AnswersResponse } from "@/controllers/answers"; import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; import authService from "@/services/auth/auth-service"; + import {AccountType} from "@dwengo-1/common/util/account-types"; const props = defineProps<{ question: QuestionDTO; @@ -80,7 +81,7 @@ {{ question.content }}
{ - await auth.loginAs("student"); + await auth.loginAs(AccountType.Student); } async function loginAsTeacher(): Promise { - await auth.loginAs("teacher"); + await auth.loginAs(AccountType.Teacher); } diff --git a/frontend/src/views/assignments/CreateAssignment.vue b/frontend/src/views/assignments/CreateAssignment.vue index 32cda330..fdbde1ce 100644 --- a/frontend/src/views/assignments/CreateAssignment.vue +++ b/frontend/src/views/assignments/CreateAssignment.vue @@ -14,6 +14,7 @@ import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; import { useRoute } from "vue-router"; + import {AccountType} from "@dwengo-1/common/util/account-types"; const route = useRoute(); const router = useRouter(); @@ -23,7 +24,7 @@ onMounted(async () => { // Redirect student - if (role.value === "student") { + if (role.value === AccountType.Student) { await router.push("/user"); } diff --git a/frontend/src/views/assignments/SingleAssignment.vue b/frontend/src/views/assignments/SingleAssignment.vue index 3d9f7f0a..bf806a59 100644 --- a/frontend/src/views/assignments/SingleAssignment.vue +++ b/frontend/src/views/assignments/SingleAssignment.vue @@ -8,9 +8,10 @@ import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; + import {AccountType} from "@dwengo-1/common/util/account-types"; const role = auth.authState.activeRole; - const isTeacher = computed(() => role === "teacher"); + const isTeacher = computed(() => role === AccountType.Teacher); const route = useRoute(); const classId = ref(route.params.classId as string); diff --git a/frontend/src/views/assignments/UserAssignments.vue b/frontend/src/views/assignments/UserAssignments.vue index afb7a380..f1594a7c 100644 --- a/frontend/src/views/assignments/UserAssignments.vue +++ b/frontend/src/views/assignments/UserAssignments.vue @@ -9,6 +9,7 @@ import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; import { asyncComputed } from "@vueuse/core"; import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; + import {AccountType} from "@dwengo-1/common/util/account-types"; const { t } = useI18n(); const router = useRouter(); @@ -16,7 +17,7 @@ const role = ref(auth.authState.activeRole); const username = ref(""); - const isTeacher = computed(() => role.value === "teacher"); + const isTeacher = computed(() => role.value === AccountType.Teacher); // Fetch and store all the teacher's classes let classesQueryResults = undefined; diff --git a/frontend/src/views/classes/UserClasses.vue b/frontend/src/views/classes/UserClasses.vue index 566bd02a..fe730300 100644 --- a/frontend/src/views/classes/UserClasses.vue +++ b/frontend/src/views/classes/UserClasses.vue @@ -2,6 +2,7 @@ import authState from "@/services/auth/auth-service.ts"; import TeacherClasses from "./TeacherClasses.vue"; import StudentClasses from "./StudentClasses.vue"; + import {AccountType} from "@dwengo-1/common/util/account-types"; // Determine if role is student or teacher to render correct view const role: string = authState.authState.activeRole!; @@ -9,7 +10,7 @@ diff --git a/frontend/src/views/learning-paths/LearningPathPage.vue b/frontend/src/views/learning-paths/LearningPathPage.vue index dc444156..865668fe 100644 --- a/frontend/src/views/learning-paths/LearningPathPage.vue +++ b/frontend/src/views/learning-paths/LearningPathPage.vue @@ -22,6 +22,7 @@ import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; import QuestionNotification from "@/components/QuestionNotification.vue"; + import {AccountType} from "@dwengo-1/common/util/account-types"; const router = useRouter(); const route = useRoute(); @@ -235,8 +236,8 @@

- - +