Merge remote-tracking branch 'origin/dev' into feature/own-learning-objects
# Conflicts: # backend/package.json # backend/src/config.ts # backend/src/controllers/learningObjects.ts # backend/src/controllers/learningPaths.ts # backend/src/data/content/attachment-repository.ts # backend/src/data/content/learning-object-repository.ts # backend/src/data/content/learning-path-repository.ts # backend/src/data/repositories.ts # backend/src/entities/content/learning-path.entity.ts # backend/src/exceptions.ts # backend/src/routes/learning-objects.ts # backend/src/services/learningObjects.ts # backend/src/services/learningPaths.ts # backend/src/util/apiHelper.ts # backend/src/util/envvars.ts # package-lock.json
This commit is contained in:
		
						commit
						cd0a3a8a7b
					
				
					 119 changed files with 8837 additions and 1697 deletions
				
			
		|  | @ -1,6 +1,5 @@ | |||
| import express, { Express, Response } from 'express'; | ||||
| import { initORM } from './orm.js'; | ||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||
| 
 | ||||
| import themeRoutes from './routes/themes.js'; | ||||
| import learningPathRoutes from './routes/learning-paths.js'; | ||||
|  | @ -12,14 +11,27 @@ import assignmentRouter from './routes/assignment.js'; | |||
| import submissionRouter from './routes/submission.js'; | ||||
| import classRouter from './routes/class.js'; | ||||
| import questionRouter from './routes/question.js'; | ||||
| import loginRouter from './routes/login.js'; | ||||
| import authRouter from './routes/auth.js'; | ||||
| import { authenticateUser } from './middleware/auth/auth.js'; | ||||
| import cors from './middleware/cors.js'; | ||||
| import { getLogger, Logger } from './logging/initalize.js'; | ||||
| import { responseTimeLogger } from './logging/responseTimeLogger.js'; | ||||
| import responseTime from 'response-time'; | ||||
| import { EnvVars, getNumericEnvVar } from './util/envvars.js'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| const app: Express = express(); | ||||
| const port: string | number = getNumericEnvVar(EnvVars.Port); | ||||
| 
 | ||||
| app.use(cors); | ||||
| app.use(express.json()); | ||||
| app.use(responseTime(responseTimeLogger)); | ||||
| app.use(authenticateUser); | ||||
| 
 | ||||
| // TODO Replace with Express routes
 | ||||
| app.get('/', (_, res: Response) => { | ||||
|     logger.debug('GET /'); | ||||
|     res.json({ | ||||
|         message: 'Hello Dwengo!🚀', | ||||
|     }); | ||||
|  | @ -31,8 +43,7 @@ app.use('/assignment', assignmentRouter); | |||
| app.use('/submission', submissionRouter); | ||||
| app.use('/class', classRouter); | ||||
| app.use('/question', questionRouter); | ||||
| app.use('/login', loginRouter); | ||||
| 
 | ||||
| app.use('/auth', authRouter); | ||||
| app.use('/theme', themeRoutes); | ||||
| app.use('/learningPath', learningPathRoutes); | ||||
| app.use('/learningObject', learningObjectRoutes); | ||||
|  | @ -41,7 +52,7 @@ async function startServer() { | |||
|     await initORM(); | ||||
| 
 | ||||
|     app.listen(port, () => { | ||||
|         console.log(`Server is running at http://localhost:${port}`); | ||||
|         logger.info(`Server is running at http://localhost:${port}`); | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,12 +1,9 @@ | |||
| // Can be placed in dotenv but found it redundant
 | ||||
| 
 | ||||
| // Import dotenv from "dotenv";
 | ||||
| 
 | ||||
| // Load .env file
 | ||||
| // Dotenv.config();
 | ||||
| 
 | ||||
| import {EnvVars, getEnvVar} from "./util/envvars"; | ||||
| 
 | ||||
| // API
 | ||||
| export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); | ||||
| 
 | ||||
| export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); | ||||
| 
 | ||||
| // Logging
 | ||||
| export const LOG_LEVEL: string = 'development' === process.env.NODE_ENV ? 'debug' : 'info'; | ||||
| export const LOKI_HOST: string = process.env.LOKI_HOST || 'http://localhost:3102'; | ||||
|  |  | |||
							
								
								
									
										33
									
								
								backend/src/controllers/auth.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										33
									
								
								backend/src/controllers/auth.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,33 @@ | |||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| 
 | ||||
| type FrontendIdpConfig = { | ||||
|     authority: string; | ||||
|     clientId: string; | ||||
|     scope: string; | ||||
|     responseType: string; | ||||
| }; | ||||
| 
 | ||||
| type FrontendAuthConfig = { | ||||
|     student: FrontendIdpConfig; | ||||
|     teacher: FrontendIdpConfig; | ||||
| }; | ||||
| 
 | ||||
| const SCOPE = 'openid profile email'; | ||||
| const RESPONSE_TYPE = 'code'; | ||||
| 
 | ||||
| export function getFrontendAuthConfig(): FrontendAuthConfig { | ||||
|     return { | ||||
|         student: { | ||||
|             authority: getEnvVar(EnvVars.IdpStudentUrl), | ||||
|             clientId: getEnvVar(EnvVars.IdpStudentClientId), | ||||
|             scope: SCOPE, | ||||
|             responseType: RESPONSE_TYPE, | ||||
|         }, | ||||
|         teacher: { | ||||
|             authority: getEnvVar(EnvVars.IdpTeacherUrl), | ||||
|             clientId: getEnvVar(EnvVars.IdpTeacherClientId), | ||||
|             scope: SCOPE, | ||||
|             responseType: RESPONSE_TYPE, | ||||
|         }, | ||||
|     }; | ||||
| } | ||||
							
								
								
									
										0
									
								
								backend/src/controllers/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/controllers/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										45
									
								
								backend/src/controllers/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										45
									
								
								backend/src/controllers/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,45 @@ | |||
| 
 | ||||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { fetchLearningPaths, searchLearningPaths } from '../services/learningPaths.js'; | ||||
| import { getLogger } from '../logging/initalize.js'; | ||||
| /** | ||||
|  * Fetch learning paths based on query parameters. | ||||
|  */ | ||||
| export async function getLearningPaths(req: Request, res: Response): Promise<void> { | ||||
|     try { | ||||
|         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; | ||||
| 
 | ||||
|         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 { | ||||
|                 res.status(404).json({ | ||||
|                     error: `Theme "${themeKey}" not found.`, | ||||
|                 }); | ||||
|                 return; | ||||
|             } | ||||
|         } else if (searchQuery) { | ||||
|             const searchResults = await searchLearningPaths(searchQuery, language); | ||||
|             res.json(searchResults); | ||||
|             return; | ||||
|         } else { | ||||
|             hruidList = themes.flatMap((theme) => theme.hruids); | ||||
|         } | ||||
| 
 | ||||
|         const learningPaths = await fetchLearningPaths(hruidList, language, `HRUIDs: ${hruidList.join(', ')}`); | ||||
|         res.json(learningPaths.data); | ||||
|     } catch (error) { | ||||
|         getLogger().error('❌ Unexpected error fetching learning paths:', error); | ||||
|         res.status(500).json({ error: 'Internal server error' }); | ||||
|     } | ||||
| } | ||||
|  | @ -1,7 +1,6 @@ | |||
| import { Request, Response } from 'express'; | ||||
| import { themes } from '../data/themes.js'; | ||||
| import { loadTranslations } from "../util/translationHelper.js"; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { loadTranslations } from '../util/translationHelper.js'; | ||||
| 
 | ||||
| interface Translations { | ||||
|     curricula_page: { | ||||
|  | @ -12,24 +11,19 @@ interface Translations { | |||
| export function getThemes(req: Request, res: Response) { | ||||
|     const language = (req.query.language as string)?.toLowerCase() || 'nl'; | ||||
|     const translations = loadTranslations<Translations>(language); | ||||
|     const themeList = themes.map((theme) => { | ||||
|         return { | ||||
|             key: theme.title, | ||||
|             title: | ||||
|                 translations.curricula_page[theme.title]?.title || theme.title, | ||||
|             description: translations.curricula_page[theme.title]?.description, | ||||
|             image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, | ||||
|         }; | ||||
|     }); | ||||
|     const themeList = themes.map((theme) => ({ | ||||
|         key: theme.title, | ||||
|         title: translations.curricula_page[theme.title]?.title || theme.title, | ||||
|         description: translations.curricula_page[theme.title]?.description, | ||||
|         image: `https://dwengo.org/images/curricula/logo_${theme.title}.png`, | ||||
|     })); | ||||
| 
 | ||||
|     res.json(themeList); | ||||
| } | ||||
| 
 | ||||
| export function getThemeByTitle(req: Request, res: Response) { | ||||
|     const themeKey = req.params.theme; | ||||
|     const theme = themes.find((t) => { | ||||
|         return t.title === themeKey; | ||||
|     }); | ||||
|     const theme = themes.find((t) => t.title === themeKey); | ||||
| 
 | ||||
|     if (theme) { | ||||
|         res.json(theme.hruids); | ||||
|  |  | |||
|  | @ -3,10 +3,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; | |||
| import { Class } from '../../entities/classes/class.entity.js'; | ||||
| 
 | ||||
| export class AssignmentRepository extends DwengoEntityRepository<Assignment> { | ||||
|     public findByClassAndId( | ||||
|         within: Class, | ||||
|         id: number | ||||
|     ): Promise<Assignment | null> { | ||||
|     public findByClassAndId(within: Class, id: number): Promise<Assignment | null> { | ||||
|         return this.findOne({ within: within, id: id }); | ||||
|     } | ||||
|     public findAllAssignmentsInClass(within: Class): Promise<Assignment[]> { | ||||
|  |  | |||
|  | @ -3,24 +3,16 @@ import { Group } from '../../entities/assignments/group.entity.js'; | |||
| import { Assignment } from '../../entities/assignments/assignment.entity.js'; | ||||
| 
 | ||||
| export class GroupRepository extends DwengoEntityRepository<Group> { | ||||
|     public findByAssignmentAndGroupNumber( | ||||
|         assignment: Assignment, | ||||
|         groupNumber: number | ||||
|     ): Promise<Group | null> { | ||||
|     public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise<Group | null> { | ||||
|         return this.findOne({ | ||||
|             assignment: assignment, | ||||
|             groupNumber: groupNumber, | ||||
|         }); | ||||
|     } | ||||
|     public findAllGroupsForAssignment( | ||||
|         assignment: Assignment | ||||
|     ): Promise<Group[]> { | ||||
|     public findAllGroupsForAssignment(assignment: Assignment): Promise<Group[]> { | ||||
|         return this.findAll({ where: { assignment: assignment } }); | ||||
|     } | ||||
|     public deleteByAssignmentAndGroupNumber( | ||||
|         assignment: Assignment, | ||||
|         groupNumber: number | ||||
|     ) { | ||||
|     public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { | ||||
|         return this.deleteWhere({ | ||||
|             assignment: assignment, | ||||
|             groupNumber: groupNumber, | ||||
|  |  | |||
|  | @ -5,10 +5,7 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object | |||
| import { Student } from '../../entities/users/student.entity.js'; | ||||
| 
 | ||||
| export class SubmissionRepository extends DwengoEntityRepository<Submission> { | ||||
|     public findSubmissionByLearningObjectAndSubmissionNumber( | ||||
|         loId: LearningObjectIdentifier, | ||||
|         submissionNumber: number | ||||
|     ): Promise<Submission | null> { | ||||
|     public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<Submission | null> { | ||||
|         return this.findOne({ | ||||
|             learningObjectHruid: loId.hruid, | ||||
|             learningObjectLanguage: loId.language, | ||||
|  | @ -17,10 +14,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> { | |||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     public findMostRecentSubmissionForStudent( | ||||
|         loId: LearningObjectIdentifier, | ||||
|         submitter: Student | ||||
|     ): Promise<Submission | null> { | ||||
|     public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise<Submission | null> { | ||||
|         return this.findOne( | ||||
|             { | ||||
|                 learningObjectHruid: loId.hruid, | ||||
|  | @ -32,10 +26,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> { | |||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public findMostRecentSubmissionForGroup( | ||||
|         loId: LearningObjectIdentifier, | ||||
|         group: Group | ||||
|     ): Promise<Submission | null> { | ||||
|     public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise<Submission | null> { | ||||
|         return this.findOne( | ||||
|             { | ||||
|                 learningObjectHruid: loId.hruid, | ||||
|  | @ -47,10 +38,7 @@ export class SubmissionRepository extends DwengoEntityRepository<Submission> { | |||
|         ); | ||||
|     } | ||||
| 
 | ||||
|     public deleteSubmissionByLearningObjectAndSubmissionNumber( | ||||
|         loId: LearningObjectIdentifier, | ||||
|         submissionNumber: number | ||||
|     ): Promise<void> { | ||||
|     public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise<void> { | ||||
|         return this.deleteWhere({ | ||||
|             learningObjectHruid: loId.hruid, | ||||
|             learningObjectLanguage: loId.language, | ||||
|  |  | |||
|  | @ -4,24 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent | |||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export class TeacherInvitationRepository extends DwengoEntityRepository<TeacherInvitation> { | ||||
|     public findAllInvitationsForClass( | ||||
|         clazz: Class | ||||
|     ): Promise<TeacherInvitation[]> { | ||||
|     public findAllInvitationsForClass(clazz: Class): Promise<TeacherInvitation[]> { | ||||
|         return this.findAll({ where: { class: clazz } }); | ||||
|     } | ||||
|     public findAllInvitationsBy(sender: Teacher): Promise<TeacherInvitation[]> { | ||||
|         return this.findAll({ where: { sender: sender } }); | ||||
|     } | ||||
|     public findAllInvitationsFor( | ||||
|         receiver: Teacher | ||||
|     ): Promise<TeacherInvitation[]> { | ||||
|     public findAllInvitationsFor(receiver: Teacher): Promise<TeacherInvitation[]> { | ||||
|         return this.findAll({ where: { receiver: receiver } }); | ||||
|     } | ||||
|     public deleteBy( | ||||
|         clazz: Class, | ||||
|         sender: Teacher, | ||||
|         receiver: Teacher | ||||
|     ): Promise<void> { | ||||
|     public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise<void> { | ||||
|         return this.deleteWhere({ | ||||
|             sender: sender, | ||||
|             receiver: receiver, | ||||
|  |  | |||
|  | @ -1,16 +1,14 @@ | |||
| import { EntityRepository, FilterQuery } from '@mikro-orm/core'; | ||||
| 
 | ||||
| export abstract class DwengoEntityRepository< | ||||
|     T extends object, | ||||
| > extends EntityRepository<T> { | ||||
| export abstract class DwengoEntityRepository<T extends object> extends EntityRepository<T> { | ||||
|     public async save(entity: T) { | ||||
|         let em = this.getEntityManager(); | ||||
|         const em = this.getEntityManager(); | ||||
|         em.persist(entity); | ||||
|         await em.flush(); | ||||
|     } | ||||
|     public async deleteWhere(query: FilterQuery<T>) { | ||||
|         let toDelete = await this.findOne(query); | ||||
|         let em = this.getEntityManager(); | ||||
|         const toDelete = await this.findOne(query); | ||||
|         const em = this.getEntityManager(); | ||||
|         if (toDelete) { | ||||
|             em.remove(toDelete); | ||||
|             await em.flush(); | ||||
|  |  | |||
|  | @ -4,12 +4,8 @@ import { Question } from '../../entities/questions/question.entity.js'; | |||
| import { Teacher } from '../../entities/users/teacher.entity.js'; | ||||
| 
 | ||||
| export class AnswerRepository extends DwengoEntityRepository<Answer> { | ||||
|     public createAnswer(answer: { | ||||
|         toQuestion: Question; | ||||
|         author: Teacher; | ||||
|         content: string; | ||||
|     }): Promise<Answer> { | ||||
|         let answerEntity = new Answer(); | ||||
|     public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise<Answer> { | ||||
|         const answerEntity = new Answer(); | ||||
|         answerEntity.toQuestion = answer.toQuestion; | ||||
|         answerEntity.author = answer.author; | ||||
|         answerEntity.content = answer.content; | ||||
|  | @ -21,10 +17,7 @@ export class AnswerRepository extends DwengoEntityRepository<Answer> { | |||
|             orderBy: { sequenceNumber: 'ASC' }, | ||||
|         }); | ||||
|     } | ||||
|     public removeAnswerByQuestionAndSequenceNumber( | ||||
|         question: Question, | ||||
|         sequenceNumber: number | ||||
|     ): Promise<void> { | ||||
|     public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise<void> { | ||||
|         return this.deleteWhere({ | ||||
|             toQuestion: question, | ||||
|             sequenceNumber: sequenceNumber, | ||||
|  |  | |||
|  | @ -4,12 +4,8 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object | |||
| import { Student } from '../../entities/users/student.entity.js'; | ||||
| 
 | ||||
| export class QuestionRepository extends DwengoEntityRepository<Question> { | ||||
|     public createQuestion(question: { | ||||
|         loId: LearningObjectIdentifier; | ||||
|         author: Student; | ||||
|         content: string; | ||||
|     }): Promise<Question> { | ||||
|         let questionEntity = new Question(); | ||||
|     public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise<Question> { | ||||
|         const questionEntity = new Question(); | ||||
|         questionEntity.learningObjectHruid = question.loId.hruid; | ||||
|         questionEntity.learningObjectLanguage = question.loId.language; | ||||
|         questionEntity.learningObjectVersion = question.loId.version; | ||||
|  | @ -17,9 +13,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | |||
|         questionEntity.content = question.content; | ||||
|         return this.insert(questionEntity); | ||||
|     } | ||||
|     public findAllQuestionsAboutLearningObject( | ||||
|         loId: LearningObjectIdentifier | ||||
|     ): Promise<Question[]> { | ||||
|     public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise<Question[]> { | ||||
|         return this.findAll({ | ||||
|             where: { | ||||
|                 learningObjectHruid: loId.hruid, | ||||
|  | @ -31,10 +25,7 @@ export class QuestionRepository extends DwengoEntityRepository<Question> { | |||
|             }, | ||||
|         }); | ||||
|     } | ||||
|     public removeQuestionByLearningObjectAndSequenceNumber( | ||||
|         loId: LearningObjectIdentifier, | ||||
|         sequenceNumber: number | ||||
|     ): Promise<void> { | ||||
|     public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise<void> { | ||||
|         return this.deleteWhere({ | ||||
|             learningObjectHruid: loId.hruid, | ||||
|             learningObjectLanguage: loId.language, | ||||
|  |  | |||
|  | @ -1,9 +1,4 @@ | |||
| import { | ||||
|     AnyEntity, | ||||
|     EntityManager, | ||||
|     EntityName, | ||||
|     EntityRepository, | ||||
| } from '@mikro-orm/core'; | ||||
| import { AnyEntity, EntityManager, EntityName, EntityRepository } from '@mikro-orm/core'; | ||||
| import { forkEntityManager } from '../orm.js'; | ||||
| import { StudentRepository } from './users/student-repository.js'; | ||||
| import { Student } from '../entities/users/student.entity.js'; | ||||
|  | @ -45,9 +40,7 @@ export function transactional<T>(f: () => Promise<T>) { | |||
|     entityManager?.transactional(f); | ||||
| } | ||||
| 
 | ||||
| function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>( | ||||
|     entity: EntityName<T> | ||||
| ): () => R { | ||||
| function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>(entity: EntityName<T>): () => R { | ||||
|     let cachedRepo: R | undefined; | ||||
|     return (): R => { | ||||
|         if (!cachedRepo) { | ||||
|  | @ -62,49 +55,22 @@ function repositoryGetter<T extends AnyEntity, R extends EntityRepository<T>>( | |||
| 
 | ||||
| /* Users */ | ||||
| export const getUserRepository = repositoryGetter<User, UserRepository>(User); | ||||
| export const getStudentRepository = repositoryGetter< | ||||
|     Student, | ||||
|     StudentRepository | ||||
| >(Student); | ||||
| export const getTeacherRepository = repositoryGetter< | ||||
|     Teacher, | ||||
|     TeacherRepository | ||||
| >(Teacher); | ||||
| export const getStudentRepository = repositoryGetter<Student, StudentRepository>(Student); | ||||
| export const getTeacherRepository = repositoryGetter<Teacher, TeacherRepository>(Teacher); | ||||
| 
 | ||||
| /* Classes */ | ||||
| export const getClassRepository = repositoryGetter<Class, ClassRepository>( | ||||
|     Class | ||||
| ); | ||||
| export const getClassJoinRequestRepository = repositoryGetter< | ||||
|     ClassJoinRequest, | ||||
|     ClassJoinRequestRepository | ||||
| >(ClassJoinRequest); | ||||
| export const getTeacherInvitationRepository = repositoryGetter< | ||||
|     TeacherInvitation, | ||||
|     TeacherInvitationRepository | ||||
| >(TeacherInvitationRepository); | ||||
| export const getClassRepository = repositoryGetter<Class, ClassRepository>(Class); | ||||
| export const getClassJoinRequestRepository = repositoryGetter<ClassJoinRequest, ClassJoinRequestRepository>(ClassJoinRequest); | ||||
| export const getTeacherInvitationRepository = repositoryGetter<TeacherInvitation, TeacherInvitationRepository>(TeacherInvitationRepository); | ||||
| 
 | ||||
| /* Assignments */ | ||||
| export const getAssignmentRepository = repositoryGetter< | ||||
|     Assignment, | ||||
|     AssignmentRepository | ||||
| >(Assignment); | ||||
| export const getGroupRepository = repositoryGetter<Group, GroupRepository>( | ||||
|     Group | ||||
| ); | ||||
| export const getSubmissionRepository = repositoryGetter< | ||||
|     Submission, | ||||
|     SubmissionRepository | ||||
| >(Submission); | ||||
| export const getAssignmentRepository = repositoryGetter<Assignment, AssignmentRepository>(Assignment); | ||||
| export const getGroupRepository = repositoryGetter<Group, GroupRepository>(Group); | ||||
| export const getSubmissionRepository = repositoryGetter<Submission, SubmissionRepository>(Submission); | ||||
| 
 | ||||
| /* Questions and answers */ | ||||
| export const getQuestionRepository = repositoryGetter< | ||||
|     Question, | ||||
|     QuestionRepository | ||||
| >(Question); | ||||
| export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>( | ||||
|     Answer | ||||
| ); | ||||
| export const getQuestionRepository = repositoryGetter<Question, QuestionRepository>(Question); | ||||
| export const getAnswerRepository = repositoryGetter<Answer, AnswerRepository>(Answer); | ||||
| 
 | ||||
| /* Learning content */ | ||||
| export const getLearningObjectRepository = repositoryGetter< | ||||
|  |  | |||
|  | @ -23,13 +23,7 @@ export const themes: Theme[] = [ | |||
|     }, | ||||
|     { | ||||
|         title: 'art', | ||||
|         hruids: [ | ||||
|             'pn_werking', | ||||
|             'un_artificiele_intelligentie', | ||||
|             'art1', | ||||
|             'art2', | ||||
|             'art3', | ||||
|         ], | ||||
|         hruids: ['pn_werking', 'un_artificiele_intelligentie', 'art1', 'art2', 'art3'], | ||||
|     }, | ||||
|     { | ||||
|         title: 'socialrobot', | ||||
|  | @ -37,12 +31,7 @@ export const themes: Theme[] = [ | |||
|     }, | ||||
|     { | ||||
|         title: 'agriculture', | ||||
|         hruids: [ | ||||
|             'pn_werking', | ||||
|             'un_artificiele_intelligentie', | ||||
|             'agri_landbouw', | ||||
|             'agri_lopendeband', | ||||
|         ], | ||||
|         hruids: ['pn_werking', 'un_artificiele_intelligentie', 'agri_landbouw', 'agri_lopendeband'], | ||||
|     }, | ||||
|     { | ||||
|         title: 'wegostem', | ||||
|  | @ -83,16 +72,7 @@ export const themes: Theme[] = [ | |||
|     }, | ||||
|     { | ||||
|         title: 'python_programming', | ||||
|         hruids: [ | ||||
|             'pn_werking', | ||||
|             'pn_datatypes', | ||||
|             'pn_operatoren', | ||||
|             'pn_structuren', | ||||
|             'pn_functies', | ||||
|             'art2', | ||||
|             'stem_insectbooks', | ||||
|             'un_algoenprog', | ||||
|         ], | ||||
|         hruids: ['pn_werking', 'pn_datatypes', 'pn_operatoren', 'pn_structuren', 'pn_functies', 'art2', 'stem_insectbooks', 'un_algoenprog'], | ||||
|     }, | ||||
|     { | ||||
|         title: 'stem', | ||||
|  | @ -110,15 +90,7 @@ export const themes: Theme[] = [ | |||
|     }, | ||||
|     { | ||||
|         title: 'care', | ||||
|         hruids: [ | ||||
|             'pn_werking', | ||||
|             'un_artificiele_intelligentie', | ||||
|             'aiz1_zorg', | ||||
|             'aiz2_grafen', | ||||
|             'aiz3_unplugged', | ||||
|             'aiz4_eindtermen', | ||||
|             'aiz5_triage', | ||||
|         ], | ||||
|         hruids: ['pn_werking', 'un_artificiele_intelligentie', 'aiz1_zorg', 'aiz2_grafen', 'aiz3_unplugged', 'aiz4_eindtermen', 'aiz5_triage'], | ||||
|     }, | ||||
|     { | ||||
|         title: 'chatbot', | ||||
|  |  | |||
|  | @ -1,11 +1,4 @@ | |||
| import { | ||||
|     Entity, | ||||
|     Enum, | ||||
|     ManyToOne, | ||||
|     OneToMany, | ||||
|     PrimaryKey, | ||||
|     Property, | ||||
| } from '@mikro-orm/core'; | ||||
| import { Entity, Enum, ManyToOne, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Class } from '../classes/class.entity.js'; | ||||
| import { Group } from './group.entity.js'; | ||||
| import { Language } from '../content/language.js'; | ||||
|  |  | |||
|  | @ -5,12 +5,17 @@ import {GroupRepository} from "../../data/assignments/group-repository"; | |||
| 
 | ||||
| @Entity({repository: () => GroupRepository}) | ||||
| export class Group { | ||||
|     @ManyToOne({ entity: () => Assignment, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Assignment, | ||||
|         primary: true, | ||||
|     }) | ||||
|     assignment!: Assignment; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'integer' }) | ||||
|     groupNumber!: number; | ||||
| 
 | ||||
|     @ManyToMany({ entity: () => Student }) | ||||
|     @ManyToMany({ | ||||
|         entity: () => Student, | ||||
|     }) | ||||
|     members!: Student[]; | ||||
| } | ||||
|  |  | |||
|  | @ -9,7 +9,10 @@ export class Submission { | |||
|     @PrimaryKey({ type: 'string' }) | ||||
|     learningObjectHruid!: string; | ||||
| 
 | ||||
|     @Enum({ items: () => Language, primary: true }) | ||||
|     @Enum({ | ||||
|         items: () => Language, | ||||
|         primary: true, | ||||
|     }) | ||||
|     learningObjectLanguage!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|  | @ -18,13 +21,18 @@ export class Submission { | |||
|     @PrimaryKey({ type: 'integer' }) | ||||
|     submissionNumber!: number; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Student }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Student, | ||||
|     }) | ||||
|     submitter!: Student; | ||||
| 
 | ||||
|     @Property({ type: 'datetime' }) | ||||
|     submissionTime!: Date; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Group, nullable: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Group, | ||||
|         nullable: true, | ||||
|     }) | ||||
|     onBehalfOf?: Group; | ||||
| 
 | ||||
|     @Property({ type: 'json' }) | ||||
|  |  | |||
|  | @ -5,10 +5,16 @@ import {ClassJoinRequestRepository} from "../../data/classes/class-join-request- | |||
| 
 | ||||
| @Entity({repository: () => ClassJoinRequestRepository}) | ||||
| export class ClassJoinRequest { | ||||
|     @ManyToOne({ entity: () => Student, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Student, | ||||
|         primary: true, | ||||
|     }) | ||||
|     requester!: Student; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Class, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Class, | ||||
|         primary: true, | ||||
|     }) | ||||
|     class!: Class; | ||||
| 
 | ||||
|     @Enum(() => ClassJoinRequestStatus) | ||||
|  |  | |||
|  | @ -1,10 +1,4 @@ | |||
| import { | ||||
|     Collection, | ||||
|     Entity, | ||||
|     ManyToMany, | ||||
|     PrimaryKey, | ||||
|     Property, | ||||
| } from '@mikro-orm/core'; | ||||
| import { Collection, Entity, ManyToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { v4 } from 'uuid'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
| import { Student } from '../users/student.entity.js'; | ||||
|  |  | |||
|  | @ -8,12 +8,21 @@ import {TeacherInvitationRepository} from "../../data/classes/teacher-invitation | |||
|  */ | ||||
| @Entity({repository: () => TeacherInvitationRepository}) | ||||
| export class TeacherInvitation { | ||||
|     @ManyToOne({ entity: () => Teacher, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Teacher, | ||||
|         primary: true, | ||||
|     }) | ||||
|     sender!: Teacher; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Teacher, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Teacher, | ||||
|         primary: true, | ||||
|     }) | ||||
|     receiver!: Teacher; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Class, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Class, | ||||
|         primary: true, | ||||
|     }) | ||||
|     class!: Class; | ||||
| } | ||||
|  |  | |||
|  | @ -4,7 +4,10 @@ import {AttachmentRepository} from "../../data/content/attachment-repository"; | |||
| 
 | ||||
| @Entity({repository: () => AttachmentRepository}) | ||||
| export class Attachment { | ||||
|     @ManyToOne({ entity: () => LearningObject, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => LearningObject, | ||||
|         primary: true, | ||||
|     }) | ||||
|     learningObject!: LearningObject; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|  |  | |||
|  | @ -1,13 +1,4 @@ | |||
| import { | ||||
|     Embeddable, | ||||
|     Embedded, | ||||
|     Entity, | ||||
|     Enum, | ||||
|     ManyToMany, | ||||
|     OneToMany, | ||||
|     PrimaryKey, | ||||
|     Property, | ||||
| } from '@mikro-orm/core'; | ||||
| import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; | ||||
| import { Language } from './language.js'; | ||||
| import { Attachment } from './attachment.entity.js'; | ||||
| import { Teacher } from '../users/teacher.entity.js'; | ||||
|  | @ -20,7 +11,10 @@ export class LearningObject { | |||
|     @PrimaryKey({ type: 'string' }) | ||||
|     hruid!: string; | ||||
| 
 | ||||
|     @Enum({ items: () => Language, primary: true }) | ||||
|     @Enum({ | ||||
|         items: () => Language, | ||||
|         primary: true, | ||||
|     }) | ||||
|     language!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'number' }) | ||||
|  | @ -29,7 +23,9 @@ export class LearningObject { | |||
|     @Property({type: 'uuid', unique: true}) | ||||
|     uuid = v4(); | ||||
| 
 | ||||
|     @ManyToMany({ entity: () => Teacher }) | ||||
|     @ManyToMany({ | ||||
|         entity: () => Teacher, | ||||
|     }) | ||||
|     admins!: Teacher[]; | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|  | @ -53,7 +49,10 @@ export class LearningObject { | |||
|     @Property({ type: 'array' }) | ||||
|     skosConcepts!: string[]; | ||||
| 
 | ||||
|     @Embedded({ entity: () => EducationalGoal, array: true }) | ||||
|     @Embedded({ | ||||
|         entity: () => EducationalGoal, | ||||
|         array: true, | ||||
|     }) | ||||
|     educationalGoals: EducationalGoal[] = []; | ||||
| 
 | ||||
|     @Property({ type: 'string' }) | ||||
|  | @ -68,7 +67,9 @@ export class LearningObject { | |||
|     @Property({ type: 'integer' }) | ||||
|     estimatedTime!: number; | ||||
| 
 | ||||
|     @Embedded({ entity: () => ReturnValue }) | ||||
|     @Embedded({ | ||||
|         entity: () => ReturnValue, | ||||
|     }) | ||||
|     returnValue!: ReturnValue; | ||||
| 
 | ||||
|     @Property({ type: 'bool' }) | ||||
|  | @ -77,7 +78,10 @@ export class LearningObject { | |||
|     @Property({ type: 'string', nullable: true }) | ||||
|     contentLocation?: string; | ||||
| 
 | ||||
|     @OneToMany({ entity: () => Attachment, mappedBy: 'learningObject' }) | ||||
|     @OneToMany({ | ||||
|         entity: () => Attachment, | ||||
|         mappedBy: 'learningObject', | ||||
|     }) | ||||
|     attachments: Attachment[] = []; | ||||
| 
 | ||||
|     @Property({ type: 'blob' }) | ||||
|  |  | |||
|  | @ -5,10 +5,16 @@ import {AnswerRepository} from "../../data/questions/answer-repository"; | |||
| 
 | ||||
| @Entity({repository: () => AnswerRepository}) | ||||
| export class Answer { | ||||
|     @ManyToOne({ entity: () => Teacher, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Teacher, | ||||
|         primary: true, | ||||
|     }) | ||||
|     author!: Teacher; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Question, primary: true }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Question, | ||||
|         primary: true, | ||||
|     }) | ||||
|     toQuestion!: Question; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'integer' }) | ||||
|  |  | |||
|  | @ -8,7 +8,10 @@ export class Question { | |||
|     @PrimaryKey({ type: 'string' }) | ||||
|     learningObjectHruid!: string; | ||||
| 
 | ||||
|     @Enum({ items: () => Language, primary: true }) | ||||
|     @Enum({ | ||||
|         items: () => Language, | ||||
|         primary: true, | ||||
|     }) | ||||
|     learningObjectLanguage!: Language; | ||||
| 
 | ||||
|     @PrimaryKey({ type: 'string' }) | ||||
|  | @ -17,7 +20,9 @@ export class Question { | |||
|     @PrimaryKey({ type: 'integer' }) | ||||
|     sequenceNumber!: number; | ||||
| 
 | ||||
|     @ManyToOne({ entity: () => Student }) | ||||
|     @ManyToOne({ | ||||
|         entity: () => Student, | ||||
|     }) | ||||
|     author!: Student; | ||||
| 
 | ||||
|     @Property({ type: 'datetime' }) | ||||
|  |  | |||
|  | @ -4,7 +4,9 @@ import { Class } from '../classes/class.entity.js'; | |||
| import { Group } from '../assignments/group.entity.js'; | ||||
| import { StudentRepository } from '../../data/users/student-repository.js'; | ||||
| 
 | ||||
| @Entity({ repository: () => StudentRepository }) | ||||
| @Entity({ | ||||
|     repository: () => StudentRepository, | ||||
| }) | ||||
| export class Student extends User { | ||||
|     @ManyToMany(() => Class) | ||||
|     classes!: Collection<Class>; | ||||
|  |  | |||
|  | @ -9,6 +9,26 @@ export class BadRequestException extends Error { | |||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 401 Unauthorized | ||||
|  */ | ||||
| export class UnauthorizedException extends Error { | ||||
|     status = 401; | ||||
|     constructor(message: string = 'Unauthorized') { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 403 Forbidden | ||||
|  */ | ||||
| export class ForbiddenException extends Error { | ||||
|     status = 403; | ||||
|     constructor(message: string = 'Forbidden') { | ||||
|         super(message); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Exception for HTTP 404 Not Found | ||||
|  */ | ||||
|  |  | |||
							
								
								
									
										53
									
								
								backend/src/logging/initalize.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										53
									
								
								backend/src/logging/initalize.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,53 @@ | |||
| import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; | ||||
| import LokiTransport from 'winston-loki'; | ||||
| import { LokiLabels } from 'loki-logger-ts'; | ||||
| import { LOG_LEVEL, LOKI_HOST } from '../config.js'; | ||||
| 
 | ||||
| export class Logger extends WinstonLogger { | ||||
|     constructor() { | ||||
|         super(); | ||||
|     } | ||||
| } | ||||
| 
 | ||||
| const Labels: LokiLabels = { | ||||
|     source: 'Dwengo-Backend', | ||||
|     service: 'API', | ||||
|     host: 'localhost', | ||||
| }; | ||||
| 
 | ||||
| let logger: Logger; | ||||
| 
 | ||||
| function initializeLogger(): Logger { | ||||
|     if (logger !== undefined) { | ||||
|         return logger; | ||||
|     } | ||||
| 
 | ||||
|     const lokiTransport: LokiTransport = new LokiTransport({ | ||||
|         host: LOKI_HOST, | ||||
|         labels: Labels, | ||||
|         level: LOG_LEVEL, | ||||
|         json: true, | ||||
|         format: format.combine(format.timestamp(), format.json()), | ||||
|         onConnectionError: (err) => { | ||||
|             // eslint-disable-next-line no-console
 | ||||
|             console.error(`Connection error: ${err}`); | ||||
|         }, | ||||
|     }); | ||||
| 
 | ||||
|     const consoleTransport = new transports.Console({ | ||||
|         level: LOG_LEVEL, | ||||
|         format: format.combine(format.cli(), format.colorize()), | ||||
|     }); | ||||
| 
 | ||||
|     logger = createLogger({ | ||||
|         transports: [lokiTransport, consoleTransport], | ||||
|     }); | ||||
| 
 | ||||
|     logger.debug(`Logger initialized with level ${LOG_LEVEL}, Loki host ${LOKI_HOST}`); | ||||
|     return logger; | ||||
| } | ||||
| 
 | ||||
| export function getLogger(): Logger { | ||||
|     logger ||= initializeLogger(); | ||||
|     return logger; | ||||
| } | ||||
							
								
								
									
										69
									
								
								backend/src/logging/mikroOrmLogger.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										69
									
								
								backend/src/logging/mikroOrmLogger.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,69 @@ | |||
| import { DefaultLogger, LogContext, LoggerNamespace } from '@mikro-orm/core'; | ||||
| import { getLogger, Logger } from './initalize.js'; | ||||
| import { LokiLabels } from 'loki-logger-ts'; | ||||
| 
 | ||||
| export class MikroOrmLogger extends DefaultLogger { | ||||
|     private logger: Logger = getLogger(); | ||||
| 
 | ||||
|     log(namespace: LoggerNamespace, message: string, context?: LogContext) { | ||||
|         if (!this.isEnabled(namespace, context)) { | ||||
|             return; | ||||
|         } | ||||
| 
 | ||||
|         switch (namespace) { | ||||
|             case 'query': | ||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'query-params': | ||||
|                 // TODO Which log level should this be?
 | ||||
|                 this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'schema': | ||||
|                 this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'discovery': | ||||
|                 this.logger.debug(this.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'info': | ||||
|                 this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             case 'deprecated': | ||||
|                 this.logger.warn(this.createMessage(namespace, message, context)); | ||||
|                 break; | ||||
|             default: | ||||
|                 switch (context?.level) { | ||||
|                     case 'info': | ||||
|                         this.logger.info(this.createMessage(namespace, message, context)); | ||||
|                         break; | ||||
|                     case 'warning': | ||||
|                         this.logger.warn(message); | ||||
|                         break; | ||||
|                     case 'error': | ||||
|                         this.logger.error(message); | ||||
|                         break; | ||||
|                     default: | ||||
|                         this.logger.debug(message); | ||||
|                         break; | ||||
|                 } | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     private createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext) { | ||||
|         const labels: LokiLabels = { | ||||
|             service: 'ORM', | ||||
|         }; | ||||
| 
 | ||||
|         let message: string; | ||||
|         if (context?.label) { | ||||
|             message = `[${namespace}] (${context?.label}) ${messageArg}`; | ||||
|         } else { | ||||
|             message = `[${namespace}] ${messageArg}`; | ||||
|         } | ||||
| 
 | ||||
|         return { | ||||
|             message: message, | ||||
|             labels: labels, | ||||
|             context: context, | ||||
|         }; | ||||
|     } | ||||
| } | ||||
							
								
								
									
										21
									
								
								backend/src/logging/responseTimeLogger.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								backend/src/logging/responseTimeLogger.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,21 @@ | |||
| import { getLogger, Logger } from './initalize.js'; | ||||
| import { Request, Response } from 'express'; | ||||
| 
 | ||||
| export function responseTimeLogger(req: Request, res: Response, time: number) { | ||||
|     const logger: Logger = getLogger(); | ||||
| 
 | ||||
|     const method = req.method; | ||||
|     const url = req.url; | ||||
|     const status = res.statusCode; | ||||
| 
 | ||||
|     logger.info({ | ||||
|         message: 'Request completed', | ||||
|         method: method, | ||||
|         url: url, | ||||
|         status: status, | ||||
|         responseTime: Number(time), | ||||
|         labels: { | ||||
|             type: 'responseTime', | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
							
								
								
									
										141
									
								
								backend/src/middleware/auth/auth.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										141
									
								
								backend/src/middleware/auth/auth.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,141 @@ | |||
| import { EnvVars, getEnvVar } from '../../util/envvars.js'; | ||||
| import { expressjwt } from 'express-jwt'; | ||||
| import { JwtPayload } from 'jsonwebtoken'; | ||||
| import jwksClient from 'jwks-rsa'; | ||||
| import * as express from 'express'; | ||||
| import * as jwt from 'jsonwebtoken'; | ||||
| import { AuthenticatedRequest } from './authenticated-request.js'; | ||||
| import { AuthenticationInfo } from './authentication-info.js'; | ||||
| import { ForbiddenException, UnauthorizedException } from '../../exceptions'; | ||||
| 
 | ||||
| const JWKS_CACHE = true; | ||||
| const JWKS_RATE_LIMIT = true; | ||||
| const REQUEST_PROPERTY_FOR_JWT_PAYLOAD = 'jwtPayload'; | ||||
| const JWT_ALGORITHM = 'RS256'; // Not configurable via env vars since supporting other algorithms would
 | ||||
| // Require additional libraries to be added.
 | ||||
| 
 | ||||
| const JWT_PROPERTY_NAMES = { | ||||
|     username: 'preferred_username', | ||||
|     firstName: 'given_name', | ||||
|     lastName: 'family_name', | ||||
|     name: 'name', | ||||
|     email: 'email', | ||||
| }; | ||||
| 
 | ||||
| function createJwksClient(uri: string): jwksClient.JwksClient { | ||||
|     return jwksClient({ | ||||
|         cache: JWKS_CACHE, | ||||
|         rateLimit: JWKS_RATE_LIMIT, | ||||
|         jwksUri: uri, | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| const idpConfigs = { | ||||
|     student: { | ||||
|         issuer: getEnvVar(EnvVars.IdpStudentUrl), | ||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), | ||||
|     }, | ||||
|     teacher: { | ||||
|         issuer: getEnvVar(EnvVars.IdpTeacherUrl), | ||||
|         jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), | ||||
|     }, | ||||
| }; | ||||
| 
 | ||||
| /** | ||||
|  * Express middleware which verifies the JWT Bearer token if one is given in the request. | ||||
|  */ | ||||
| const verifyJwtToken = expressjwt({ | ||||
|     secret: async (_: express.Request, token: jwt.Jwt | undefined) => { | ||||
|         if (!token?.payload || !(token.payload as JwtPayload).iss) { | ||||
|             throw new Error('Invalid token'); | ||||
|         } | ||||
| 
 | ||||
|         const issuer = (token.payload as JwtPayload).iss; | ||||
| 
 | ||||
|         const idpConfig = Object.values(idpConfigs).find((config) => config.issuer === issuer); | ||||
|         if (!idpConfig) { | ||||
|             throw new Error('Issuer not accepted.'); | ||||
|         } | ||||
| 
 | ||||
|         const signingKey = await idpConfig.jwksClient.getSigningKey(token.header.kid); | ||||
|         if (!signingKey) { | ||||
|             throw new Error('Signing key not found.'); | ||||
|         } | ||||
|         return signingKey.getPublicKey(); | ||||
|     }, | ||||
|     audience: getEnvVar(EnvVars.IdpAudience), | ||||
|     algorithms: [JWT_ALGORITHM], | ||||
|     credentialsRequired: false, | ||||
|     requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, | ||||
| }); | ||||
| 
 | ||||
| /** | ||||
|  * Get an object with information about the authenticated user from a given authenticated request. | ||||
|  */ | ||||
| function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { | ||||
|     if (!req.jwtPayload) { | ||||
|         return; | ||||
|     } | ||||
|     const issuer = req.jwtPayload.iss; | ||||
|     let accountType: 'student' | 'teacher'; | ||||
| 
 | ||||
|     if (issuer === idpConfigs.student.issuer) { | ||||
|         accountType = 'student'; | ||||
|     } else if (issuer === idpConfigs.teacher.issuer) { | ||||
|         accountType = 'teacher'; | ||||
|     } else { | ||||
|         return; | ||||
|     } | ||||
|     return { | ||||
|         accountType: accountType, | ||||
|         username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, | ||||
|         name: req.jwtPayload[JWT_PROPERTY_NAMES.name], | ||||
|         firstName: req.jwtPayload[JWT_PROPERTY_NAMES.firstName], | ||||
|         lastName: req.jwtPayload[JWT_PROPERTY_NAMES.lastName], | ||||
|         email: req.jwtPayload[JWT_PROPERTY_NAMES.email], | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| /** | ||||
|  * Add the AuthenticationInfo object with the information about the current authentication to the request in order | ||||
|  * to avoid that the routers have to deal with the JWT token. | ||||
|  */ | ||||
| const addAuthenticationInfo = (req: AuthenticatedRequest, res: express.Response, next: express.NextFunction) => { | ||||
|     req.auth = getAuthenticationInfo(req); | ||||
|     next(); | ||||
| }; | ||||
| 
 | ||||
| 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 const authorize = | ||||
|     (accessCondition: (auth: AuthenticationInfo) => boolean) => | ||||
|     (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'); | ||||
							
								
								
									
										9
									
								
								backend/src/middleware/auth/authenticated-request.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										9
									
								
								backend/src/middleware/auth/authenticated-request.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,9 @@ | |||
| import { Request } from 'express'; | ||||
| import { JwtPayload } from 'jsonwebtoken'; | ||||
| import { AuthenticationInfo } from './authentication-info.js'; | ||||
| 
 | ||||
| export interface AuthenticatedRequest extends Request { | ||||
|     // Properties are optional since the user is not necessarily authenticated.
 | ||||
|     jwtPayload?: JwtPayload; | ||||
|     auth?: AuthenticationInfo; | ||||
| } | ||||
							
								
								
									
										11
									
								
								backend/src/middleware/auth/authentication-info.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										11
									
								
								backend/src/middleware/auth/authentication-info.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1,11 @@ | |||
| /** | ||||
|  * Object with information about the user who is currently logged in. | ||||
|  */ | ||||
| export type AuthenticationInfo = { | ||||
|     accountType: 'student' | 'teacher'; | ||||
|     username: string; | ||||
|     name?: string; | ||||
|     firstName?: string; | ||||
|     lastName?: string; | ||||
|     email?: string; | ||||
| }; | ||||
							
								
								
									
										7
									
								
								backend/src/middleware/cors.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										7
									
								
								backend/src/middleware/cors.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,7 @@ | |||
| import cors from 'cors'; | ||||
| import { EnvVars, getEnvVar } from '../util/envvars.js'; | ||||
| 
 | ||||
| export default cors({ | ||||
|     origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), | ||||
|     allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), | ||||
| }); | ||||
|  | @ -1,7 +1,9 @@ | |||
| import { Options } from '@mikro-orm/core'; | ||||
| import { LoggerOptions, Options } from '@mikro-orm/core'; | ||||
| import { PostgreSqlDriver } from '@mikro-orm/postgresql'; | ||||
| import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; | ||||
| import { SqliteDriver } from '@mikro-orm/sqlite'; | ||||
| import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; | ||||
| import { LOG_LEVEL } from './config.js'; | ||||
| 
 | ||||
| // Import alle entity-bestanden handmatig
 | ||||
| import { User } from './entities/users/user.entity.js'; | ||||
|  | @ -24,11 +26,20 @@ import { Answer } from './entities/questions/answer.entity.js'; | |||
| import { Question } from './entities/questions/question.entity.js'; | ||||
| 
 | ||||
| const entities = [ | ||||
|     User, Student, Teacher, | ||||
|     Assignment, Group, Submission, | ||||
|     Class, ClassJoinRequest, TeacherInvitation, | ||||
|     Attachment, LearningObject, LearningPath, | ||||
|     Answer, Question | ||||
|     User, | ||||
|     Student, | ||||
|     Teacher, | ||||
|     Assignment, | ||||
|     Group, | ||||
|     Submission, | ||||
|     Class, | ||||
|     ClassJoinRequest, | ||||
|     TeacherInvitation, | ||||
|     Attachment, | ||||
|     LearningObject, | ||||
|     LearningPath, | ||||
|     Answer, | ||||
|     Question, | ||||
| ]; | ||||
| 
 | ||||
| function config(testingMode: boolean = false): Options { | ||||
|  | @ -37,25 +48,28 @@ function config(testingMode: boolean = false): Options { | |||
|             driver: SqliteDriver, | ||||
|             dbName: getEnvVar(EnvVars.DbName), | ||||
|             entities: entities, | ||||
|             // entitiesTs: entitiesTs,
 | ||||
|             // EntitiesTs: entitiesTs,
 | ||||
| 
 | ||||
|             // Workaround: vitest: `TypeError: Unknown file extension ".ts"` (ERR_UNKNOWN_FILE_EXTENSION)
 | ||||
|             // (see https://mikro-orm.io/docs/guide/project-setup#testing-the-endpoint)
 | ||||
|             dynamicImportProvider: (id) => import(id), | ||||
|         }; | ||||
|     } else { | ||||
|         return { | ||||
|             driver: PostgreSqlDriver, | ||||
|             host: getEnvVar(EnvVars.DbHost), | ||||
|             port: getNumericEnvVar(EnvVars.DbPort), | ||||
|             dbName: getEnvVar(EnvVars.DbName), | ||||
|             user: getEnvVar(EnvVars.DbUsername), | ||||
|             password: getEnvVar(EnvVars.DbPassword), | ||||
|             entities: entities, | ||||
|             //entitiesTs: entitiesTs,
 | ||||
|             debug: true, | ||||
|         }; | ||||
|     } | ||||
| 
 | ||||
|     return { | ||||
|         driver: PostgreSqlDriver, | ||||
|         host: getEnvVar(EnvVars.DbHost), | ||||
|         port: getNumericEnvVar(EnvVars.DbPort), | ||||
|         dbName: getEnvVar(EnvVars.DbName), | ||||
|         user: getEnvVar(EnvVars.DbUsername), | ||||
|         password: getEnvVar(EnvVars.DbPassword), | ||||
|         entities: entities, | ||||
|         // EntitiesTs: entitiesTs,
 | ||||
| 
 | ||||
|         // Logging
 | ||||
|         debug: LOG_LEVEL === 'debug', | ||||
|         loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), | ||||
|     }; | ||||
| } | ||||
| 
 | ||||
| export default config; | ||||
|  |  | |||
|  | @ -1,9 +1,15 @@ | |||
| import { EntityManager, MikroORM } from '@mikro-orm/core'; | ||||
| import config from './mikro-orm.config.js'; | ||||
| import { EnvVars, getEnvVar } from './util/envvars.js'; | ||||
| import { getLogger, Logger } from './logging/initalize.js'; | ||||
| 
 | ||||
| let orm: MikroORM | undefined; | ||||
| export async function initORM(testingMode: boolean = false) { | ||||
|     const logger: Logger = getLogger(); | ||||
| 
 | ||||
|     logger.info('Initializing ORM'); | ||||
|     logger.debug('MikroORM config is', config); | ||||
| 
 | ||||
|     orm = await MikroORM.init(config(testingMode)); | ||||
|     // Update the database scheme if necessary and enabled.
 | ||||
|     if (getEnvVar(EnvVars.DbUpdate)) { | ||||
|  | @ -22,9 +28,7 @@ export async function initORM(testingMode: boolean = false) { | |||
| } | ||||
| export function forkEntityManager(): EntityManager { | ||||
|     if (!orm) { | ||||
|         throw Error( | ||||
|             'Accessing the Entity Manager before the ORM is fully initialized.' | ||||
|         ); | ||||
|         throw Error('Accessing the Entity Manager before the ORM is fully initialized.'); | ||||
|     } | ||||
|     return orm.em.fork(); | ||||
| } | ||||
|  |  | |||
|  | @ -1,23 +1,20 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         assignments: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         assignments: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about an assignment with id 'id'
 | ||||
| // Information about an assignment with id 'id'
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|         title: 'Dit is een test assignment', | ||||
|         description: 'Een korte beschrijving', | ||||
|         groups: [ '0' ], | ||||
|         groups: ['0'], | ||||
|         learningPath: '0', | ||||
|         class: '0', | ||||
|         links: { | ||||
|  | @ -25,30 +22,24 @@ router.get('/:id', (req, res) => { | |||
|             submissions: `${req.baseUrl}/${req.params.id}`, | ||||
|         }, | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/submissions', (req, res) => { | ||||
|     res.json({ | ||||
|         submissions: [ | ||||
|             '0' | ||||
|         ], | ||||
|         submissions: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/groups', (req, res) => { | ||||
|     res.json({ | ||||
|         groups: [ | ||||
|             '0' | ||||
|         ], | ||||
|         groups: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/questions', (req, res) => { | ||||
|     res.json({ | ||||
|         questions: [ | ||||
|             '0' | ||||
|         ], | ||||
|         questions: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
							
								
								
									
										23
									
								
								backend/src/routes/auth.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										23
									
								
								backend/src/routes/auth.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,23 @@ | |||
| import express from 'express'; | ||||
| import { getFrontendAuthConfig } from '../controllers/auth.js'; | ||||
| import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/auth/auth.js'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // Returns auth configuration for frontend
 | ||||
| router.get('/config', (req, res) => { | ||||
|     res.json(getFrontendAuthConfig()); | ||||
| }); | ||||
| 
 | ||||
| router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { | ||||
|     res.json({ message: 'If you see this, you should be authenticated!' }); | ||||
| }); | ||||
| 
 | ||||
| router.get('/testStudentsOnly', studentsOnly, (req, res) => { | ||||
|     res.json({ message: 'If you see this, you should be a student!' }); | ||||
| }); | ||||
| 
 | ||||
| router.get('/testTeachersOnly', teachersOnly, (req, res) => { | ||||
|     res.json({ message: 'If you see this, you should be a teacher!' }); | ||||
| }); | ||||
| 
 | ||||
| export default router; | ||||
|  | @ -1,55 +1,46 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         classes: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         classes: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about an class with id 'id'
 | ||||
| // Information about an class with id 'id'
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|         displayName: 'Klas 4B', | ||||
|         teachers: [ '0' ], | ||||
|         students: [ '0' ], | ||||
|         joinRequests: [ '0' ], | ||||
|         teachers: ['0'], | ||||
|         students: ['0'], | ||||
|         joinRequests: ['0'], | ||||
|         links: { | ||||
|             self: `${req.baseUrl}/${req.params.id}`, | ||||
|             classes: `${req.baseUrl}/${req.params.id}/invitations`, | ||||
|             questions: `${req.baseUrl}/${req.params.id}/assignments`, | ||||
|             students: `${req.baseUrl}/${req.params.id}/students`, | ||||
|         } | ||||
|         }, | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/invitations', (req, res) => { | ||||
|     res.json({ | ||||
|         invitations: [  | ||||
|             '0' | ||||
|         ], | ||||
|         invitations: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/assignments', (req, res) => { | ||||
|     res.json({ | ||||
|         assignments: [  | ||||
|             '0' | ||||
|         ], | ||||
|         assignments: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/students', (req, res) => { | ||||
|     res.json({ | ||||
|         students: [  | ||||
|             '0' | ||||
|         ], | ||||
|         students: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,34 +1,31 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         groups: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         groups: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about a group (members, ... [TODO DOC])
 | ||||
| // Information about a group (members, ... [TODO DOC])
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|         assignment: '0', | ||||
|         students: [ '0' ], | ||||
|         submissions: [ '0' ], | ||||
|         // reference to other endpoint
 | ||||
|         // should be less hardcoded
 | ||||
|         questions: `/group/${req.params.id}/question`,  | ||||
|         students: ['0'], | ||||
|         submissions: ['0'], | ||||
|         // Reference to other endpoint
 | ||||
|         // Should be less hardcoded
 | ||||
|         questions: `/group/${req.params.id}/question`, | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| // the list of questions a group has made
 | ||||
| // The list of questions a group has made
 | ||||
| router.get('/:id/question', (req, res) => { | ||||
|     res.json({ | ||||
|         questions: [ '0' ], | ||||
|         questions: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,14 +0,0 @@ | |||
| import express from 'express' | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // returns login paths for IDP
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         // dummy variables, needs to be changed
 | ||||
|         // with IDP endpoints
 | ||||
|         leerkracht: '/login-leerkracht', | ||||
|         leerling: '/login-leerling', | ||||
|     }); | ||||
| }) | ||||
| 
 | ||||
| export default router | ||||
|  | @ -1,17 +1,14 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         questions: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         questions: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about an question with id 'id'
 | ||||
| // Information about an question with id 'id'
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|  | @ -23,16 +20,14 @@ router.get('/:id', (req, res) => { | |||
|         links: { | ||||
|             self: `${req.baseUrl}/${req.params.id}`, | ||||
|             answers: `${req.baseUrl}/${req.params.id}/answers`, | ||||
|         } | ||||
|         }, | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| router.get('/:id/answers', (req, res) => { | ||||
|     res.json({ | ||||
|         answers: [ | ||||
|             '0' | ||||
|         ], | ||||
|     }) | ||||
| }) | ||||
|         answers: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,17 +1,14 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         students: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         students: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about a student's profile
 | ||||
| // Information about a student's profile
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|  | @ -27,33 +24,32 @@ router.get('/:id', (req, res) => { | |||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // the list of classes a student is in
 | ||||
| // The list of classes a student is in
 | ||||
| router.get('/:id/classes', (req, res) => { | ||||
|     res.json({ | ||||
|         classes: [ '0' ], | ||||
|         classes: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| // the list of submissions a student has made
 | ||||
| // The list of submissions a student has made
 | ||||
| router.get('/:id/submissions', (req, res) => { | ||||
|     res.json({ | ||||
|         submissions: [ '0' ], | ||||
|         submissions: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
|    | ||||
| // the list of assignments a student has
 | ||||
| // The list of assignments a student has
 | ||||
| router.get('/:id/assignments', (req, res) => { | ||||
|     res.json({ | ||||
|         assignments: [ '0' ], | ||||
|         assignments: ['0'], | ||||
|     }); | ||||
| }) | ||||
|    | ||||
| // the list of groups a student is in
 | ||||
| }); | ||||
| 
 | ||||
| // The list of groups a student is in
 | ||||
| router.get('/:id/groups', (req, res) => { | ||||
|     res.json({ | ||||
|         groups: [ '0' ], | ||||
|         groups: ['0'], | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,17 +1,14 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         submissions: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         submissions: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about an submission with id 'id'
 | ||||
| // Information about an submission with id 'id'
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|  | @ -21,6 +18,6 @@ router.get('/:id', (req, res) => { | |||
|         content: 'Wortel 2 is rationeel', | ||||
|         learningObject: '0', | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
|  | @ -1,17 +1,14 @@ | |||
| import express from 'express' | ||||
| import express from 'express'; | ||||
| const router = express.Router(); | ||||
| 
 | ||||
| // root endpoint used to search objects
 | ||||
| // Root endpoint used to search objects
 | ||||
| router.get('/', (req, res) => { | ||||
|     res.json({ | ||||
|         teachers: [ | ||||
|             '0', | ||||
|             '1', | ||||
|         ] | ||||
|         teachers: ['0', '1'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // information about a teacher
 | ||||
| // Information about a teacher
 | ||||
| router.get('/:id', (req, res) => { | ||||
|     res.json({ | ||||
|         id: req.params.id, | ||||
|  | @ -25,34 +22,27 @@ router.get('/:id', (req, res) => { | |||
|             invitations: `${req.baseUrl}/${req.params.id}/invitations`, | ||||
|         }, | ||||
|     }); | ||||
| }) | ||||
| }); | ||||
| 
 | ||||
| // the questions students asked a teacher
 | ||||
| // The questions students asked a teacher
 | ||||
| router.get('/:id/questions', (req, res) => { | ||||
|     res.json({ | ||||
|         questions: [ | ||||
|             '0' | ||||
|         ], | ||||
|         questions: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // invitations to other classes a teacher received
 | ||||
| // Invitations to other classes a teacher received
 | ||||
| router.get('/:id/invitations', (req, res) => { | ||||
|     res.json({ | ||||
|         invitations: [ | ||||
|             '0' | ||||
|         ], | ||||
|         invitations: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| // a list with ids of classes a teacher is in
 | ||||
| // A list with ids of classes a teacher is in
 | ||||
| router.get('/:id/classes', (req, res) => { | ||||
|     res.json({ | ||||
|         classes: [ | ||||
|             '0' | ||||
|         ], | ||||
|         classes: ['0'], | ||||
|     }); | ||||
| }); | ||||
| 
 | ||||
| 
 | ||||
| export default router | ||||
| export default router; | ||||
|  |  | |||
							
								
								
									
										0
									
								
								backend/src/services/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/services/learningObjects.ts
									
										
									
									
									
										Normal file
									
								
							
							
								
								
									
										0
									
								
								backend/src/services/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										0
									
								
								backend/src/services/learningPaths.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -1,6 +1,7 @@ | |||
| import axios, { AxiosRequestConfig } from 'axios'; | ||||
| import { getLogger, Logger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| // !!!! when logger is done -> change
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| /** | ||||
|  * Utility function to fetch data from an API endpoint with error handling. | ||||
|  | @ -28,19 +29,14 @@ export async function fetchWithLogging<T>( | |||
|     } catch (error: any) { | ||||
|         if (error.response) { | ||||
|             if (error.response.status === 404) { | ||||
|                 console.error( | ||||
|                     `❌ ERROR: ${description} not found (404) at "${url}".` | ||||
|                 ); | ||||
|                 logger.debug(`❌ ERROR: ${description} not found (404) at "${url}".`); | ||||
|             } else { | ||||
|                 console.error( | ||||
|                 logger.debug( | ||||
|                     `❌ ERROR: Failed to fetch ${description}. Status: ${error.response.status} - ${error.response.statusText} (URL: "${url}")` | ||||
|                 ); | ||||
|             } | ||||
|         } else { | ||||
|             console.error( | ||||
|                 `❌ ERROR: Network or unexpected error when fetching ${description}:`, | ||||
|                 error.message | ||||
|             ); | ||||
|             logger.debug(`❌ ERROR: Network or unexpected error when fetching ${description}:`, error.message); | ||||
|         } | ||||
|         return null; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,5 +1,9 @@ | |||
| const PREFIX = 'DWENGO_'; | ||||
| const DB_PREFIX = PREFIX + 'DB_'; | ||||
| const IDP_PREFIX = PREFIX + 'AUTH_'; | ||||
| const STUDENT_IDP_PREFIX = IDP_PREFIX + 'STUDENT_'; | ||||
| const TEACHER_IDP_PREFIX = IDP_PREFIX + 'TEACHER_'; | ||||
| const CORS_PREFIX = PREFIX + 'CORS_'; | ||||
| 
 | ||||
| type EnvVar = { key: string; required?: boolean; defaultValue?: any }; | ||||
| 
 | ||||
|  | @ -14,6 +18,15 @@ export const EnvVars: { [key: string]: EnvVar } = { | |||
|     LearningContentRepoApiBaseUrl: { key: PREFIX + "LEARNING_CONTENT_REPO_API_BASE_URL", defaultValue: "https://dwengo.org/backend/api"}, | ||||
|     FallbackLanguage: { key: PREFIX + "FALLBACK_LANGUAGE", defaultValue: "nl" }, | ||||
|     UserContentPrefix: { key: DB_PREFIX + 'USER_CONTENT_PREFIX', defaultValue: "u_" }, | ||||
|     IdpStudentUrl: { key: STUDENT_IDP_PREFIX + 'URL', required: true }, | ||||
|     IdpStudentClientId: { key: STUDENT_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||
|     IdpStudentJwksEndpoint: { key: STUDENT_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||
|     IdpTeacherUrl: { key: TEACHER_IDP_PREFIX + 'URL', required: true }, | ||||
|     IdpTeacherClientId: { key: TEACHER_IDP_PREFIX + 'CLIENT_ID', required: true }, | ||||
|     IdpTeacherJwksEndpoint: { key: TEACHER_IDP_PREFIX + 'JWKS_ENDPOINT', required: true }, | ||||
|     IdpAudience: { key: IDP_PREFIX + 'AUDIENCE', defaultValue: 'account' }, | ||||
|     CorsAllowedOrigins: { key: CORS_PREFIX + 'ALLOWED_ORIGINS', defaultValue: '' }, | ||||
|     CorsAllowedHeaders: { key: CORS_PREFIX + 'ALLOWED_HEADERS', defaultValue: 'Authorization,Content-Type' }, | ||||
| } as const; | ||||
| 
 | ||||
| /** | ||||
|  | @ -39,9 +52,7 @@ export function getNumericEnvVar(envVar: EnvVar): number { | |||
|     const valueString = getEnvVar(envVar); | ||||
|     const value = parseInt(valueString); | ||||
|     if (isNaN(value)) { | ||||
|         throw new Error( | ||||
|             `Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.` | ||||
|         ); | ||||
|         throw new Error(`Invalid value for environment variable ${envVar.key}: ${valueString}. Expected a number.`); | ||||
|     } else { | ||||
|         return value; | ||||
|     } | ||||
|  |  | |||
|  | @ -1,7 +1,10 @@ | |||
| import fs from 'fs'; | ||||
| import path from 'path'; | ||||
| import yaml from 'js-yaml'; | ||||
| import {FALLBACK_LANG} from "../../config"; | ||||
| import { FALLBACK_LANG } from '../config.js'; | ||||
| import { getLogger, Logger } from '../logging/initalize.js'; | ||||
| 
 | ||||
| const logger: Logger = getLogger(); | ||||
| 
 | ||||
| export function loadTranslations<T>(language: string): T { | ||||
|     try { | ||||
|  | @ -9,10 +12,7 @@ export function loadTranslations<T>(language: string): T { | |||
|         const yamlFile = fs.readFileSync(filePath, 'utf8'); | ||||
|         return yaml.load(yamlFile) as T; | ||||
|     } catch (error) { | ||||
|         console.error( | ||||
|             `Cannot load translation for ${language}, fallen back to dutch` | ||||
|         ); | ||||
|         console.error(error); | ||||
|         logger.warn(`Cannot load translation for ${language}, fallen back to dutch`, error); | ||||
|         const fallbackPath = path.join(process.cwd(), '_i18n', `${FALLBACK_LANG}.yml`); | ||||
|         return yaml.load(fs.readFileSync(fallbackPath, 'utf8')) as T; | ||||
|     } | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger