Merge remote-tracking branch 'origin/dev' into fix/testdata-niet-meer-correct-opgezet

# Conflicts:
#	backend/tests/controllers/teachers.test.ts
This commit is contained in:
Gabriellvl 2025-05-16 12:51:18 +02:00
commit 67cf87dc9b
67 changed files with 884 additions and 335 deletions

View file

@ -10,6 +10,7 @@
### Dwengo ###
DWENGO_PORT=3000
DWENGO_RUN_MODE=test
DWENGO_DB_NAME=":memory:"
DWENGO_DB_UPDATE=true

View file

@ -1,10 +1,11 @@
import { UnauthorizedException } from '../exceptions/unauthorized-exception.js';
import { getLogger } from '../logging/initalize.js';
import { AuthenticatedRequest } from '../middleware/auth/authenticated-request.js';
import { createOrUpdateStudent } from '../services/students.js';
import { createOrUpdateTeacher } from '../services/teachers.js';
import { envVars, getEnvVar } from '../util/envVars.js';
import { Response } from 'express';
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;
@ -40,6 +41,10 @@ export function getFrontendAuthConfig(): FrontendAuthConfig {
};
}
export function handleGetFrontendAuthConfig(_req: Request, res: Response): void {
res.json(getFrontendAuthConfig());
}
export async function postHelloHandler(req: AuthenticatedRequest, res: Response): Promise<void> {
const auth = req.auth;
if (!auth) {
@ -51,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 {

View file

@ -113,7 +113,7 @@ export async function createStudentRequestHandler(req: Request, res: Response):
const classId = req.body.classId;
requireFields({ username, classId });
const request = await createClassJoinRequest(username, classId);
const request = await createClassJoinRequest(username, classId.toUpperCase());
res.json({ request });
}

View file

@ -2,6 +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.js';
export async function getAllInvitationsHandler(req: Request, res: Response): Promise<void> {
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);

View file

@ -3,9 +3,9 @@ import { Question } from '../../entities/questions/question.entity.js';
import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier.js';
import { Student } from '../../entities/users/student.entity.js';
import { LearningObject } from '../../entities/content/learning-object.entity.js';
import { Group } from '../../entities/assignments/group.entity.js';
import { Assignment } from '../../entities/assignments/assignment.entity.js';
import { Loaded } from '@mikro-orm/core';
import { Group } from '../../entities/assignments/group.entity';
export class QuestionRepository extends DwengoEntityRepository<Question> {
public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; inGroup: Group; content: string }): Promise<Question> {

View file

@ -10,6 +10,10 @@ export function mapToUserDTO(user: User): UserDTO {
};
}
export function mapToUsername(user: { username: string }): string {
return user.username;
}
export function mapToUser<T extends User>(userData: UserDTO, userInstance: T): T {
userInstance.username = userData.username;
userInstance.firstName = userData.firstName;

View file

@ -7,7 +7,6 @@ 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';
const JWKS_CACHE = true;
const JWKS_RATE_LIMIT = true;
@ -108,36 +107,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');

View file

@ -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<string, unknown> = Record<string, unknown>,
> extends Request<P, ResBody, ReqBody, ReqQuery, Locals> {
// Properties are optional since the user is not necessarily authenticated.
jwtPayload?: JwtPayload;
auth?: AuthenticationInfo;

View file

@ -0,0 +1,21 @@
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).
* 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 };
if (auth.accountType === 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));
});

View file

@ -0,0 +1,61 @@
import { AuthenticationInfo } from '../authentication-info.js';
import { AuthenticatedRequest } from '../authenticated-request.js';
import * as express from 'express';
import { RequestHandler } from 'express';
import { UnauthorizedException } from '../../../exceptions/unauthorized-exception.js';
import { ForbiddenException } from '../../../exceptions/forbidden-exception.js';
import { envVars, getEnvVar } from '../../../util/envVars.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
* the given access condition.
* @param accessCondition Predicate over the current AuthenticationInfo. Access is only granted when this evaluates
* to true.
*/
export function authorize<P, ResBody, ReqBody, ReqQuery, Locals extends Record<string, unknown>>(
accessCondition: (auth: AuthenticationInfo, req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>) => boolean | Promise<boolean>
): RequestHandler<P, ResBody, ReqBody, ReqQuery, Locals> {
// Bypass authentication during testing
if (getEnvVar(envVars.RunMode) === 'test') {
return async (
_req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
_res: express.Response,
next: express.NextFunction
): Promise<void> => {
next();
};
}
return async (
req: AuthenticatedRequest<P, ResBody, ReqBody, ReqQuery, Locals>,
_res: express.Response,
next: express.NextFunction
): Promise<void> => {
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 === AccountType.Student);
/**
* Middleware which rejects requests from unauthenticated users or users that aren't teachers.
*/
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.
*/
export const adminOnly = authorize(() => false);

View file

@ -0,0 +1,70 @@
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';
import { getAllInvitations } from '../../../services/teacher-invitations.js';
import { AccountType } from '@dwengo-1/common/util/account-types';
async function teaches(teacherUsername: string, classId: string): Promise<boolean> {
const clazz = await fetchClass(classId);
return clazz.teachers.map(mapToUsername).includes(teacherUsername);
}
/**
* 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 === 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)
);
/**
* 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 === AccountType.Teacher) {
return clazz.teachers.map(mapToUsername).includes(auth.username);
}
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 === 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.
*/
export const onlyAllowOwnClassInBody = authorize(async (auth, req) => {
const classId = (req.body as { class: string })?.class;
const clazz = await fetchClass(classId);
if (auth.accountType === AccountType.Teacher) {
return clazz.teachers.map(mapToUsername).includes(auth.username);
}
return clazz.students.map(mapToUsername).includes(auth.username);
});

View file

@ -0,0 +1,26 @@
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'.
* 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 === 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);
});

View file

@ -0,0 +1,21 @@
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')
* 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 (auth.accountType === AccountType.Student && forGroup && assignmentNo && classId) {
// TODO: groupNumber?
// Const group = await fetchGroup(Number(classId), Number(assignmentNo), )
return false;
}
return true;
});

View file

@ -0,0 +1,66 @@
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';
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
);
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 question = await fetchQuestion(questionId);
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 });
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;
});
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 question = await fetchQuestion(questionId);
const group = question.inGroup;
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
return group.members.map(mapToUsername).includes(auth.username);
});

View file

@ -0,0 +1,28 @@
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';
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
);
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 === 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);
});

View file

@ -0,0 +1,17 @@
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
);
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
);
export const onlyAllowReceiverBody = authorize(
(auth: AuthenticationInfo, req: AuthenticatedRequest) => (req.body as { receiver: string }).receiver === auth.username
);

View file

@ -0,0 +1,8 @@
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.
*/
export const preventImpersonation = authorize((auth: AuthenticationInfo, req: AuthenticatedRequest) => req.params.username === auth.username);

View file

@ -1,16 +1,18 @@
import express from 'express';
import { createAnswerHandler, deleteAnswerHandler, getAnswerHandler, getAllAnswersHandler, updateAnswerHandler } from '../controllers/answers.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 });
router.get('/', getAllAnswersHandler);
router.get('/', authenticatedOnly, getAllAnswersHandler);
router.post('/', createAnswerHandler);
router.post('/', teachersOnly, onlyAllowAuthor, createAnswerHandler);
router.get('/:seqAnswer', getAnswerHandler);
router.get('/:seqAnswer', onlyAllowIfHasAccessToQuestion, getAnswerHandler);
router.delete('/:seqAnswer', deleteAnswerHandler);
router.delete('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, deleteAnswerHandler);
router.put('/:seqAnswer', updateAnswerHandler);
router.put('/:seqAnswer', teachersOnly, onlyAllowAuthorRequestAnswer, updateAnswerHandler);
export default router;

View file

@ -9,22 +9,25 @@ import {
putAssignmentHandler,
} from '../controllers/assignments.js';
import groupRouter from './groups.js';
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 });
router.get('/', getAllAssignmentsHandler);
router.get('/', teachersOnly, onlyAllowIfInClass, getAllAssignmentsHandler);
router.post('/', createAssignmentHandler);
router.post('/', teachersOnly, onlyAllowIfInClass, createAssignmentHandler);
router.get('/:id', getAssignmentHandler);
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', getAssignmentsSubmissionsHandler);
router.get('/:id/submissions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentsSubmissionsHandler);
router.get('/:id/questions', getAssignmentQuestionsHandler);
router.get('/:id/questions', teachersOnly, onlyAllowIfHasAccessToAssignment, getAssignmentQuestionsHandler);
router.use('/:assignmentid/groups', groupRouter);

View file

@ -1,28 +1,35 @@
import express from 'express';
import { getFrontendAuthConfig, postHelloHandler } from '../controllers/auth.js';
import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/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', (_req, res) => {
res.json(getFrontendAuthConfig());
});
router.get('/config', handleGetFrontendAuthConfig);
router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */
/* #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
res.json({ message: 'If you see this, you should be authenticated!' });
});
router.get('/testStudentsOnly', studentsOnly, (_req, res) => {
/* #swagger.security = [{ "student": [ ] }] */
/* #swagger.security = [{ "studentProduction": [ ] }, { "studentStaging": [ ] }, { "studentDev": [ ] }] */
res.json({ message: 'If you see this, you should be a student!' });
});
router.get('/testTeachersOnly', teachersOnly, (_req, res) => {
/* #swagger.security = [{ "teacher": [ ] }] */
/* #swagger.security = [{ "teacherProduction": [ ] }, { "teacherStaging": [ ] }, { "teacherDev": [ ] }] */
res.json({ message: 'If you see this, you should be a teacher!' });
});
router.post('/hello', authenticatedOnly, postHelloHandler);
// 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,
/*
#swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }]
*/ postHelloHandler
);
export default router;

View file

@ -14,33 +14,35 @@ import {
putClassHandler,
} 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';
const router = express.Router();
// Root endpoint used to search objects
router.get('/', getAllClassesHandler);
router.get('/', adminOnly, getAllClassesHandler);
router.post('/', createClassHandler);
router.post('/', teachersOnly, createClassHandler);
router.get('/:id', getClassHandler);
router.get('/:id', onlyAllowIfInClassOrInvited, 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', getTeacherInvitationsHandler);
router.get('/:id/teacher-invitations', teachersOnly, onlyAllowIfInClass, getTeacherInvitationsHandler);
router.get('/:id/students', getClassStudentsHandler);
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);

View file

@ -8,22 +8,24 @@ import {
getGroupSubmissionsHandler,
putGroupHandler,
} from '../controllers/groups.js';
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 });
// Root endpoint used to search objects
router.get('/', getAllGroupsHandler);
router.get('/', onlyAllowIfHasAccessToAssignment, getAllGroupsHandler);
router.post('/', createGroupHandler);
router.post('/', teachersOnly, onlyAllowIfHasAccessToAssignment, createGroupHandler);
router.get('/:groupid', 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', getGroupSubmissionsHandler);
router.get('/:groupid/submissions', onlyAllowIfHasAccessToGroup, getGroupSubmissionsHandler);
router.get('/:groupid/questions', getGroupQuestionsHandler);
router.get('/:groupid/questions', onlyAllowIfHasAccessToGroup, getGroupQuestionsHandler);
export default router;

View file

@ -1,8 +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.js';
const router = express.Router();
@ -16,13 +16,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 +32,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;

View file

@ -1,5 +1,6 @@
import express from 'express';
import { getLearningPaths } from '../controllers/learning-paths.js';
import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js';
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;

View file

@ -1,20 +1,25 @@
import express from 'express';
import { createQuestionHandler, deleteQuestionHandler, getAllQuestionsHandler, getQuestionHandler } from '../controllers/questions.js';
import answerRoutes from './answers.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';
const router = express.Router({ mergeParams: true });
// Query language
// Root endpoint used to search objects
router.get('/', getAllQuestionsHandler);
router.get('/', authenticatedOnly, 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', onlyAllowIfHasAccessToQuestion, getQuestionHandler);
router.delete('/:seq', studentsOnly, onlyAllowAuthorRequest, deleteQuestionHandler);
router.put('/:seq', studentsOnly, onlyAllowAuthorRequest, updateAnswerHandler);
router.use('/:seq/answers', answerRoutes);

View file

@ -18,12 +18,30 @@ router.get('/', (_, res: Response) => {
});
});
router.use('/student', studentRouter /* #swagger.tags = ['Student'] */);
router.use('/teacher', teacherRouter /* #swagger.tags = ['Teacher'] */);
router.use('/class', classRouter /* #swagger.tags = ['Class'] */);
router.use('/auth', authRouter /* #swagger.tags = ['Auth'] */);
router.use('/theme', themeRoutes /* #swagger.tags = ['Theme'] */);
router.use('/learningPath', learningPathRoutes /* #swagger.tags = ['Learning Path'] */);
router.use('/learningObject', learningObjectRoutes /* #swagger.tags = ['Learning Object'] */);
router.use(
'/class',
classRouter /* #swagger.tags = ['Class'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/learningObject',
learningObjectRoutes /* #swagger.tags = ['Learning Object'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/learningPath',
learningPathRoutes /* #swagger.tags = ['Learning Path'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/student',
studentRouter /* #swagger.tags = ['Student'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/teacher',
teacherRouter /* #swagger.tags = ['Teacher'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
router.use(
'/theme',
themeRoutes /* #swagger.tags = ['Theme'], #swagger.security = [{ "studentProduction": [ ] }, { "teacherProduction": [ ] }, { "studentStaging": [ ] }, { "teacherStaging": [ ] }, { "studentDev": [ ] }, { "teacherDev": [ ] }] */
);
export default router;

View file

@ -5,15 +5,19 @@ import {
getStudentRequestHandler,
getStudentRequestsHandler,
} from '../controllers/students.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('/', getStudentRequestsHandler);
router.get('/', preventImpersonation, getStudentRequestsHandler);
router.post('/', createStudentRequestHandler);
router.post('/', preventImpersonation, createStudentRequestHandler);
router.get('/:classId', getStudentRequestHandler);
router.get('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, getStudentRequestHandler);
router.delete('/:classId', deleteClassJoinRequestHandler);
router.delete('/:classId', onlyAllowStudentHimselfAndTeachersOfClass, deleteClassJoinRequestHandler);
export default router;

View file

@ -11,33 +11,37 @@ import {
getStudentSubmissionsHandler,
} from '../controllers/students.js';
import joinRequestRouter from './student-join-requests.js';
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
import { adminOnly } from '../middleware/auth/checks/auth-checks.js';
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', deleteStudentHandler);
router.delete('/:username', preventImpersonation, deleteStudentHandler);
// Information about a student's profile
router.get('/:username', getStudentHandler);
router.get('/:username', preventImpersonation, getStudentHandler);
// The list of classes a student is in
router.get('/:username/classes', getStudentClassesHandler);
router.get('/:username/classes', preventImpersonation, getStudentClassesHandler);
// The list of submissions a student has made
router.get('/:username/submissions', getStudentSubmissionsHandler);
router.get('/:username/submissions', preventImpersonation, getStudentSubmissionsHandler);
// The list of assignments a student has
router.get('/:username/assignments', getStudentAssignmentsHandler);
router.get('/:username/assignments', preventImpersonation, getStudentAssignmentsHandler);
// The list of groups a student is in
router.get('/:username/groups', getStudentGroupsHandler);
router.get('/:username/groups', preventImpersonation, getStudentGroupsHandler);
// A list of questions a user has created
router.get('/:username/questions', getStudentQuestionsHandler);
router.get('/:username/questions', preventImpersonation, getStudentQuestionsHandler);
router.use('/:username/joinRequests', joinRequestRouter);

View file

@ -1,15 +1,15 @@
import express from 'express';
import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler, getSubmissionsHandler } from '../controllers/submissions.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('/', createSubmissionHandler);
router.post('/', 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;

View file

@ -6,17 +6,24 @@ import {
getInvitationHandler,
updateInvitationHandler,
} from '../controllers/teacher-invitations.js';
import { preventImpersonation } from '../middleware/auth/checks/user-auth-checks.js';
import {
onlyAllowReceiverBody,
onlyAllowSender,
onlyAllowSenderBody,
onlyAllowSenderOrReceiver,
} from '../middleware/auth/checks/teacher-invitation-checks.js';
const router = express.Router({ mergeParams: true });
router.get('/:username', getAllInvitationsHandler);
router.get('/:username', preventImpersonation, 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;

View file

@ -10,25 +10,27 @@ import {
updateStudentJoinRequestHandler,
} from '../controllers/teachers.js';
import invitationRouter from './teacher-invitations.js';
import { adminOnly } from '../middleware/auth/checks/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();
// 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', preventImpersonation, getTeacherHandler);
router.delete('/:username', deleteTeacherHandler);
router.delete('/:username', preventImpersonation, deleteTeacherHandler);
router.get('/:username/classes', getTeacherClassHandler);
router.get('/:username/classes', preventImpersonation, getTeacherClassHandler);
router.get('/:username/students', getTeacherStudentHandler);
router.get('/:username/students', preventImpersonation, getTeacherStudentHandler);
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.use('/invitations', invitationRouter);

View file

@ -1,14 +1,15 @@
import express from 'express';
import { getThemesHandler, getHruidsByThemeHandler } from '../controllers/themes.js';
import { authenticatedOnly } from '../middleware/auth/checks/auth-checks.js';
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;

View file

@ -34,7 +34,7 @@ export async function createAnswer(questionId: QuestionId, answerData: AnswerDat
return mapToAnswerDTO(answer);
}
async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
export async function fetchAnswer(questionId: QuestionId, sequenceNumber: number): Promise<Answer> {
const answerRepository = getAnswerRepository();
const question = await fetchQuestion(questionId);
const answer = await answerRepository.findAnswer(question, sequenceNumber);

View file

@ -34,6 +34,15 @@ export async function fetchGroup(classId: string, assignmentNumber: number, grou
return group;
}
export async function fetchAllGroups(classId: string, assignmentNumber: number): Promise<Group[]> {
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<GroupDTO> {
const group = await fetchGroup(classId, assignmentNumber, groupNumber);
return mapToGroupDTO(group, group.assignment.within);

View file

@ -13,6 +13,7 @@ import { fetchStudent } from './students.js';
import { NotFoundException } from '../exceptions/not-found-exception.js';
import { FALLBACK_VERSION_NUM } from '../config.js';
import { fetchAssignment } from './assignments.js';
import { ConflictException } from '../exceptions/conflict-exception.js';
export async function getQuestionsAboutLearningObjectInAssignment(
loId: LearningObjectIdentifier,
@ -99,10 +100,18 @@ export async function createQuestion(loId: LearningObjectIdentifier, questionDat
const inGroup = await getGroupRepository().findByAssignmentAndGroupNumber(assignment, questionData.inGroup.groupNumber);
if (!inGroup) {
throw new NotFoundException('Group with id and assignment not found');
}
if (!inGroup.members.contains(author)) {
throw new ConflictException('Author is not part of this group');
}
const question = await questionRepository.createQuestion({
loId,
author,
inGroup: inGroup!,
inGroup: inGroup,
content,
});

View file

@ -24,7 +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 { Submission } from '../entities/assignments/submission.entity.js';
import { mapToUsername } from '../interfaces/user.js';
export async function getAllStudents(full: boolean): Promise<StudentDTO[] | string[]> {
const studentRepository = getStudentRepository();
@ -34,7 +35,7 @@ export async function getAllStudents(full: boolean): Promise<StudentDTO[] | stri
return users.map(mapToStudentDTO);
}
return users.map((user) => user.username);
return users.map(mapToUsername);
}
export async function fetchStudent(username: string): Promise<Student> {
@ -64,7 +65,7 @@ export async function createStudent(userData: StudentDTO): Promise<StudentDTO> {
const newStudent = mapToStudent(userData);
await studentRepository.save(newStudent, { preventOverwrite: true });
return userData;
return mapToStudentDTO(newStudent);
}
export async function createOrUpdateStudent(userData: StudentDTO): Promise<StudentDTO> {

View file

@ -32,6 +32,10 @@ export async function createInvitation(data: TeacherInvitationData): Promise<Tea
throw new ConflictException('The teacher sending the invite is not part of the class');
}
if (cls.teachers.contains(receiver)) {
throw new ConflictException('The teacher receiving the invite is already part of the class');
}
const newInvitation = mapToInvitation(sender, receiver, cls);
await teacherInvitationRepository.save(newInvitation, { preventOverwrite: true });

View file

@ -18,6 +18,7 @@ import { StudentDTO } from '@dwengo-1/common/interfaces/student';
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.js';
export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | string[]> {
const teacherRepository: TeacherRepository = getTeacherRepository();
@ -26,7 +27,7 @@ export async function getAllTeachers(full: boolean): Promise<TeacherDTO[] | stri
if (full) {
return users.map(mapToTeacherDTO);
}
return users.map((user) => user.username);
return users.map(mapToUsername);
}
export async function fetchTeacher(username: string): Promise<Teacher> {
@ -45,7 +46,8 @@ export async function getTeacher(username: string): Promise<TeacherDTO> {
return mapToTeacherDTO(user);
}
export async function createTeacher(userData: TeacherDTO): Promise<TeacherDTO> {
// TODO update parameter
export async function createTeacher(userData: TeacherDTO, _update?: boolean): Promise<TeacherDTO> {
const teacherRepository: TeacherRepository = getTeacherRepository();
const newTeacher = mapToTeacher(userData);
@ -98,7 +100,9 @@ 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;

View file

@ -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';
import { getFooFighters, getTestleerkracht1 } from '../test_assets/users/teachers.testdata.js';
import { getClass02 } from '../test_assets/classes/classes.testdata.js';
@ -101,7 +100,7 @@ describe('Teacher controllers', () => {
});
it('Teacher list', async () => {
req = { query: { full: 'true' } };
req = { query: { full: 'false' } };
await getAllTeachersHandler(req as Request, res as Response);
@ -109,10 +108,8 @@ describe('Teacher controllers', () => {
const result = jsonMock.mock.lastCall?.[0];
const teacherUsernames = result.teachers.map((s: TeacherDTO) => s.username);
const teacher = getTestleerkracht1();
expect(teacherUsernames).toContain(teacher.username);
expect(result.teachers).toContain(teacher.username);
expect(result.teachers).toHaveLength(5);
});