diff --git a/.github/workflows/backend-testing.yml b/.github/workflows/backend-testing.yml new file mode 100644 index 00000000..0d0b1f3f --- /dev/null +++ b/.github/workflows/backend-testing.yml @@ -0,0 +1,45 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, run backend tests across different versions of node (here 22.x) +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Backend Testing + +# Workflow runs when: +# - a backend js/ts file on "dev" changes +# - a non-draft PR to "dev" with backend js/ts files is opened, is reopened, or changes +# - a draft PR to "dev" with backend js/ts files is marked as ready for review +on: + push: + branches: [ "dev" ] + paths: + - 'backend/src/**.[jt]s' + - 'backend/tests/**.[jt]s' + - 'backend/vitest.config.ts' + pull_request: + branches: [ "dev" ] + types: ["synchronize", "ready_for_review", "opened", "reopened"] + paths: + - 'backend/src/**.[jt]s' + - 'backend/tests/**.[jt]s' + - 'backend/vitest.config.ts' + + +jobs: + test: + name: Run backend unit tests + if: '! github.event.pull_request.draft' + runs-on: [self-hosted, Linux, X64] + + strategy: + matrix: + node-version: [22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit -w backend diff --git a/.github/workflows/frontend-testing.yml b/.github/workflows/frontend-testing.yml new file mode 100644 index 00000000..ff7bde4d --- /dev/null +++ b/.github/workflows/frontend-testing.yml @@ -0,0 +1,54 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, run frontend tests across different versions of node (here 22.x) +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Frontend Testing + +# Workflow runs when: +# - a frontend js/ts/vue/css file on "dev" changes +# - a non-draft PR to "dev" with frontend js/ts/vue/css files is opened, is reopened, or changes +# - a draft PR to "dev" with frontend js/ts/vue/css files is marked as ready for review +on: + push: + branches: [ "dev" ] + paths: + - 'frontend/src/**.[jt]s' + - 'frontend/src/**.vue' + - 'frontend/src/**.css' + - 'frontend/tests/**.[jt]s' + - 'frontend/tests/**.vue' + - 'frontend/tests/**.css' + - 'frontend/vitest.config.ts' + - 'frontend/playwright.config.ts' + pull_request: + branches: [ "dev" ] + types: ["synchronize", "ready_for_review", "opened", "reopened"] + paths: + - 'frontend/src/**.[jt]s' + - 'frontend/src/**.vue' + - 'frontend/src/**.css' + - 'frontend/tests/**.[jt]s' + - 'frontend/tests/**.vue' + - 'frontend/tests/**.css' + - 'frontend/vitest.config.ts' + - 'frontend/playwright.config.ts' + +jobs: + test: + name: Run frontend unit tests + if: '! github.event.pull_request.draft' + runs-on: [self-hosted, Linux, X64] + + strategy: + matrix: + node-version: [22.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: 'npm' + - run: npm ci + - run: npm run test:unit -w frontend diff --git a/backend/eslint.config.ts b/backend/eslint.config.ts index 6b696021..f5f225b2 100644 --- a/backend/eslint.config.ts +++ b/backend/eslint.config.ts @@ -8,14 +8,4 @@ export default [ globals: globals.node, }, }, - - { - files: ['tests/**/*.ts'], - languageOptions: { - globals: globals.node, - }, - rules: { - 'no-console': 'off', - }, - }, ]; diff --git a/backend/package.json b/backend/package.json index 4e3b890d..c08fb1dc 100644 --- a/backend/package.json +++ b/backend/package.json @@ -11,7 +11,7 @@ "format": "prettier --write src/", "format-check": "prettier --check src/", "lint": "eslint . --fix", - "test:unit": "vitest" + "test:unit": "vitest --run" }, "dependencies": { "@mikro-orm/core": "6.4.9", diff --git a/backend/src/app.ts b/backend/src/app.ts index 07a84126..cf10a6df 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,7 +5,7 @@ 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'; +import { envVars, getNumericEnvVar } from './util/envVars.js'; import apiRouter from './routes/router.js'; import swaggerMiddleware from './swagger.js'; import swaggerUi from 'swagger-ui-express'; @@ -14,7 +14,7 @@ import { errorHandler } from './middleware/error-handling/error-handler.js'; const logger: Logger = getLogger(); const app: Express = express(); -const port: string | number = getNumericEnvVar(EnvVars.Port); +const port: string | number = getNumericEnvVar(envVars.Port); app.use(express.json()); app.use(cors); @@ -29,7 +29,7 @@ app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); app.use(errorHandler); -async function startServer() { +async function startServer(): Promise { await initORM(); app.listen(port, () => { diff --git a/backend/src/config.ts b/backend/src/config.ts index b9974a3b..9b4702b5 100644 --- a/backend/src/config.ts +++ b/backend/src/config.ts @@ -1,7 +1,7 @@ -import { EnvVars, getEnvVar } from './util/envvars.js'; +import { envVars, getEnvVar } from './util/envVars.js'; // API -export const DWENGO_API_BASE = getEnvVar(EnvVars.LearningContentRepoApiBaseUrl); -export const FALLBACK_LANG = getEnvVar(EnvVars.FallbackLanguage); +export const DWENGO_API_BASE = getEnvVar(envVars.LearningContentRepoApiBaseUrl); +export const FALLBACK_LANG = getEnvVar(envVars.FallbackLanguage); export const FALLBACK_SEQ_NUM = 1; diff --git a/backend/src/controllers/assignments.ts b/backend/src/controllers/assignments.ts index 16dbb310..c8dd1ec8 100644 --- a/backend/src/controllers/assignments.ts +++ b/backend/src/controllers/assignments.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { createAssignment, getAllAssignments, getAssignment, getAssignmentsSubmissions } from '../services/assignments.js'; import { AssignmentDTO } from '../interfaces/assignment.js'; -// Typescript is annoy with with parameter forwarding from class.ts +// Typescript is annoying with parameter forwarding from class.ts interface AssignmentParams { classid: string; id: string; @@ -41,7 +41,7 @@ export async function createAssignmentHandler(req: Request, re } export async function getAssignmentHandler(req: Request, res: Response): Promise { - const id = +req.params.id; + const id = Number(req.params.id); const classid = req.params.classid; if (isNaN(id)) { @@ -61,7 +61,7 @@ export async function getAssignmentHandler(req: Request, res: export async function getAssignmentsSubmissionsHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const assignmentNumber = +req.params.id; + const assignmentNumber = Number(req.params.id); const full = req.query.full === 'true'; if (isNaN(assignmentNumber)) { diff --git a/backend/src/controllers/auth.ts b/backend/src/controllers/auth.ts index 409ead0c..b87eaf7b 100644 --- a/backend/src/controllers/auth.ts +++ b/backend/src/controllers/auth.ts @@ -1,16 +1,16 @@ -import { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; -type FrontendIdpConfig = { +interface FrontendIdpConfig { authority: string; clientId: string; scope: string; responseType: string; -}; +} -type FrontendAuthConfig = { +interface FrontendAuthConfig { student: FrontendIdpConfig; teacher: FrontendIdpConfig; -}; +} const SCOPE = 'openid profile email'; const RESPONSE_TYPE = 'code'; @@ -18,14 +18,14 @@ const RESPONSE_TYPE = 'code'; export function getFrontendAuthConfig(): FrontendAuthConfig { return { student: { - authority: getEnvVar(EnvVars.IdpStudentUrl), - clientId: getEnvVar(EnvVars.IdpStudentClientId), + authority: getEnvVar(envVars.IdpStudentUrl), + clientId: getEnvVar(envVars.IdpStudentClientId), scope: SCOPE, responseType: RESPONSE_TYPE, }, teacher: { - authority: getEnvVar(EnvVars.IdpTeacherUrl), - clientId: getEnvVar(EnvVars.IdpTeacherClientId), + authority: getEnvVar(envVars.IdpTeacherUrl), + clientId: getEnvVar(envVars.IdpTeacherClientId), scope: SCOPE, responseType: RESPONSE_TYPE, }, diff --git a/backend/src/controllers/groups.ts b/backend/src/controllers/groups.ts index 38d5d5d0..7de3e114 100644 --- a/backend/src/controllers/groups.ts +++ b/backend/src/controllers/groups.ts @@ -12,14 +12,14 @@ interface GroupParams { export async function getGroupHandler(req: Request, res: Response): Promise { const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const groupId = +req.params.groupid!; // Can't be undefined + const groupId = Number(req.params.groupid!); // Can't be undefined if (isNaN(groupId)) { res.status(400).json({ error: 'Group id must be a number' }); @@ -40,7 +40,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); @@ -56,7 +56,7 @@ export async function getAllGroupsHandler(req: Request, res: Response): Promise< export async function createGroupHandler(req: Request, res: Response): Promise { const classid = req.params.classid; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); @@ -78,14 +78,14 @@ export async function getGroupSubmissionsHandler(req: Request, res: Response): P const classId = req.params.classid; const full = req.query.full === 'true'; - const assignmentId = +req.params.assignmentid; + const assignmentId = Number(req.params.assignmentid); if (isNaN(assignmentId)) { res.status(400).json({ error: 'Assignment id must be a number' }); return; } - const groupId = +req.params.groupid!; // Can't be undefined + const groupId = Number(req.params.groupid); // Can't be undefined if (isNaN(groupId)) { res.status(400).json({ error: 'Group id must be a number' }); diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index 53eb1ded..fc79ef0d 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -2,7 +2,7 @@ import { Request, Response } from 'express'; import { FALLBACK_LANG } from '../config.js'; import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../interfaces/learning-content.js'; import learningObjectService from '../services/learning-objects/learning-object-service.js'; -import { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; import { Language } from '../entities/content/language.js'; import attachmentService from '../services/learning-objects/attachment-service.js'; import { NotFoundError } from '@mikro-orm/core'; @@ -13,8 +13,8 @@ function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIde throw new BadRequestException('HRUID is required.'); } return { - hruid: req.params.hruid as string, - language: (req.query.language || getEnvVar(EnvVars.FallbackLanguage)) as Language, + hruid: req.params.hruid, + language: (req.query.language || getEnvVar(envVars.FallbackLanguage)) as Language, version: parseInt(req.query.version as string), }; } @@ -24,7 +24,7 @@ function getLearningPathIdentifierFromRequest(req: Request): LearningPathIdentif throw new BadRequestException('HRUID is required.'); } return { - hruid: req.params.hruid as string, + hruid: req.params.hruid, language: (req.query.language as Language) || FALLBACK_LANG, }; } diff --git a/backend/src/controllers/questions.ts b/backend/src/controllers/questions.ts index 00a51329..6735c305 100644 --- a/backend/src/controllers/questions.ts +++ b/backend/src/controllers/questions.ts @@ -17,7 +17,7 @@ function getObjectId(req: Request, res: Response): LearningObjectIdentifier | nu return { hruid, language: (lang as Language) || FALLBACK_LANG, - version: +version, + version: Number(version), }; } diff --git a/backend/src/controllers/submissions.ts b/backend/src/controllers/submissions.ts index 512ac22e..67c1d3a9 100644 --- a/backend/src/controllers/submissions.ts +++ b/backend/src/controllers/submissions.ts @@ -10,7 +10,7 @@ interface SubmissionParams { export async function getSubmissionHandler(req: Request, res: Response): Promise { const lohruid = req.params.hruid; - const submissionNumber = +req.params.id; + const submissionNumber = Number(req.params.id); if (isNaN(submissionNumber)) { res.status(400).json({ error: 'Submission number is not a number' }); @@ -30,7 +30,7 @@ export async function getSubmissionHandler(req: Request, res: res.json(submission); } -export async function createSubmissionHandler(req: Request, res: Response) { +export async function createSubmissionHandler(req: Request, res: Response): Promise { const submissionDTO = req.body as SubmissionDTO; const submission = await createSubmission(submissionDTO); @@ -43,9 +43,9 @@ export async function createSubmissionHandler(req: Request, res: Response) { res.json(submission); } -export async function deleteSubmissionHandler(req: Request, res: Response) { +export async function deleteSubmissionHandler(req: Request, res: Response): Promise { const hruid = req.params.hruid; - const submissionNumber = +req.params.id; + const submissionNumber = Number(req.params.id); const lang = languageMap[req.query.language as string] || Language.Dutch; const version = (req.query.version || 1) as number; diff --git a/backend/src/controllers/themes.ts b/backend/src/controllers/themes.ts index 8406cf5f..b34d0c80 100644 --- a/backend/src/controllers/themes.ts +++ b/backend/src/controllers/themes.ts @@ -3,25 +3,23 @@ import { themes } from '../data/themes.js'; import { loadTranslations } from '../util/translation-helper.js'; interface Translations { - curricula_page: { - [key: string]: { title: string; description?: string }; - }; + curricula_page: Record; } -export function getThemesHandler(req: Request, res: Response) { - const language = (req.query.language as string)?.toLowerCase() || 'nl'; +export function getThemesHandler(req: Request, res: Response): void { + const language = ((req.query.language as string) || 'nl').toLowerCase(); const translations = loadTranslations(language); const themeList = themes.map((theme) => ({ key: theme.title, - title: translations.curricula_page[theme.title]?.title || theme.title, - description: translations.curricula_page[theme.title]?.description, + 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 getHruidsByThemeHandler(req: Request, res: Response) { +export function getHruidsByThemeHandler(req: Request, res: Response): void { const themeKey = req.params.theme; if (!themeKey) { diff --git a/backend/src/data/assignments/assignment-repository.ts b/backend/src/data/assignments/assignment-repository.ts index c3c457d3..3de5031d 100644 --- a/backend/src/data/assignments/assignment-repository.ts +++ b/backend/src/data/assignments/assignment-repository.ts @@ -3,13 +3,13 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; import { Class } from '../../entities/classes/class.entity.js'; export class AssignmentRepository extends DwengoEntityRepository { - public findByClassAndId(within: Class, id: number): Promise { + public async findByClassAndId(within: Class, id: number): Promise { return this.findOne({ within: within, id: id }); } - public findAllAssignmentsInClass(within: Class): Promise { + public async findAllAssignmentsInClass(within: Class): Promise { return this.findAll({ where: { within: within } }); } - public deleteByClassAndId(within: Class, id: number): Promise { + public async deleteByClassAndId(within: Class, id: number): Promise { return this.deleteWhere({ within: within, id: id }); } } diff --git a/backend/src/data/assignments/group-repository.ts b/backend/src/data/assignments/group-repository.ts index eb1b09e2..f06080f7 100644 --- a/backend/src/data/assignments/group-repository.ts +++ b/backend/src/data/assignments/group-repository.ts @@ -4,7 +4,7 @@ import { Assignment } from '../../entities/assignments/assignment.entity.js'; import { Student } from '../../entities/users/student.entity.js'; export class GroupRepository extends DwengoEntityRepository { - public findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { + public async findByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { return this.findOne( { assignment: assignment, @@ -13,16 +13,16 @@ export class GroupRepository extends DwengoEntityRepository { { populate: ['members'] } ); } - public findAllGroupsForAssignment(assignment: Assignment): Promise { + public async findAllGroupsForAssignment(assignment: Assignment): Promise { return this.findAll({ where: { assignment: assignment }, populate: ['members'], }); } - public findAllGroupsWithStudent(student: Student): Promise { + public async findAllGroupsWithStudent(student: Student): Promise { return this.find({ members: student }, { populate: ['members'] }); } - public deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number) { + public async deleteByAssignmentAndGroupNumber(assignment: Assignment, groupNumber: number): Promise { return this.deleteWhere({ assignment: assignment, groupNumber: groupNumber, diff --git a/backend/src/data/assignments/submission-repository.ts b/backend/src/data/assignments/submission-repository.ts index 251823fa..f5090adc 100644 --- a/backend/src/data/assignments/submission-repository.ts +++ b/backend/src/data/assignments/submission-repository.ts @@ -5,7 +5,10 @@ import { LearningObjectIdentifier } from '../../entities/content/learning-object import { Student } from '../../entities/users/student.entity.js'; export class SubmissionRepository extends DwengoEntityRepository { - public findSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + public async findSubmissionByLearningObjectAndSubmissionNumber( + loId: LearningObjectIdentifier, + submissionNumber: number + ): Promise { return this.findOne({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, @@ -14,7 +17,7 @@ export class SubmissionRepository extends DwengoEntityRepository { }); } - public findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { + public async findMostRecentSubmissionForStudent(loId: LearningObjectIdentifier, submitter: Student): Promise { return this.findOne( { learningObjectHruid: loId.hruid, @@ -26,7 +29,7 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise { + public async findMostRecentSubmissionForGroup(loId: LearningObjectIdentifier, group: Group): Promise { return this.findOne( { learningObjectHruid: loId.hruid, @@ -38,15 +41,15 @@ export class SubmissionRepository extends DwengoEntityRepository { ); } - public findAllSubmissionsForGroup(group: Group): Promise { + public async findAllSubmissionsForGroup(group: Group): Promise { return this.find({ onBehalfOf: group }); } - public findAllSubmissionsForStudent(student: Student): Promise { + public async findAllSubmissionsForStudent(student: Student): Promise { return this.find({ submitter: student }); } - public deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { + public async deleteSubmissionByLearningObjectAndSubmissionNumber(loId: LearningObjectIdentifier, submissionNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/backend/src/data/classes/class-join-request-repository.ts b/backend/src/data/classes/class-join-request-repository.ts index ce1dcec5..7aa94ba7 100644 --- a/backend/src/data/classes/class-join-request-repository.ts +++ b/backend/src/data/classes/class-join-request-repository.ts @@ -4,16 +4,16 @@ import { ClassJoinRequest, ClassJoinRequestStatus } from '../../entities/classes import { Student } from '../../entities/users/student.entity.js'; export class ClassJoinRequestRepository extends DwengoEntityRepository { - public findAllRequestsBy(requester: Student): Promise { + public async findAllRequestsBy(requester: Student): Promise { return this.findAll({ where: { requester: requester } }); } - public findAllOpenRequestsTo(clazz: Class): Promise { + public async findAllOpenRequestsTo(clazz: Class): Promise { return this.findAll({ where: { class: clazz, status: ClassJoinRequestStatus.Open } }); // TODO check if works like this } - public findByStudentAndClass(requester: Student, clazz: Class): Promise { + public async findByStudentAndClass(requester: Student, clazz: Class): Promise { return this.findOne({ requester, class: clazz }); } - public deleteBy(requester: Student, clazz: Class): Promise { + public async deleteBy(requester: Student, clazz: Class): Promise { return this.deleteWhere({ requester: requester, class: clazz }); } } diff --git a/backend/src/data/classes/class-repository.ts b/backend/src/data/classes/class-repository.ts index 0ceed98e..f4e0723f 100644 --- a/backend/src/data/classes/class-repository.ts +++ b/backend/src/data/classes/class-repository.ts @@ -4,20 +4,20 @@ import { Student } from '../../entities/users/student.entity.js'; import { Teacher } from '../../entities/users/teacher.entity'; export class ClassRepository extends DwengoEntityRepository { - public findById(id: string): Promise { + public async findById(id: string): Promise { return this.findOne({ classId: id }, { populate: ['students', 'teachers'] }); } - public deleteById(id: string): Promise { + public async deleteById(id: string): Promise { return this.deleteWhere({ classId: id }); } - public findByStudent(student: Student): Promise { + public async findByStudent(student: Student): Promise { return this.find( { students: student }, { populate: ['students', 'teachers'] } // Voegt student en teacher objecten toe ); } - public findByTeacher(teacher: Teacher): Promise { + public async findByTeacher(teacher: Teacher): Promise { return this.find({ teachers: teacher }, { populate: ['students', 'teachers'] }); } } diff --git a/backend/src/data/classes/teacher-invitation-repository.ts b/backend/src/data/classes/teacher-invitation-repository.ts index 6b94deec..ce059ca8 100644 --- a/backend/src/data/classes/teacher-invitation-repository.ts +++ b/backend/src/data/classes/teacher-invitation-repository.ts @@ -4,16 +4,16 @@ import { TeacherInvitation } from '../../entities/classes/teacher-invitation.ent import { Teacher } from '../../entities/users/teacher.entity.js'; export class TeacherInvitationRepository extends DwengoEntityRepository { - public findAllInvitationsForClass(clazz: Class): Promise { + public async findAllInvitationsForClass(clazz: Class): Promise { return this.findAll({ where: { class: clazz } }); } - public findAllInvitationsBy(sender: Teacher): Promise { + public async findAllInvitationsBy(sender: Teacher): Promise { return this.findAll({ where: { sender: sender } }); } - public findAllInvitationsFor(receiver: Teacher): Promise { + public async findAllInvitationsFor(receiver: Teacher): Promise { return this.findAll({ where: { receiver: receiver } }); } - public deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { + public async deleteBy(clazz: Class, sender: Teacher, receiver: Teacher): Promise { return this.deleteWhere({ sender: sender, receiver: receiver, diff --git a/backend/src/data/content/attachment-repository.ts b/backend/src/data/content/attachment-repository.ts index 95c5ab1c..73baa943 100644 --- a/backend/src/data/content/attachment-repository.ts +++ b/backend/src/data/content/attachment-repository.ts @@ -4,7 +4,7 @@ import { Language } from '../../entities/content/language'; import { LearningObjectIdentifier } from '../../entities/content/learning-object-identifier'; export class AttachmentRepository extends DwengoEntityRepository { - public findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise { + public async findByLearningObjectIdAndName(learningObjectId: LearningObjectIdentifier, name: string): Promise { return this.findOne({ learningObject: { hruid: learningObjectId.hruid, @@ -15,7 +15,11 @@ export class AttachmentRepository extends DwengoEntityRepository { }); } - public findByMostRecentVersionOfLearningObjectAndName(hruid: string, language: Language, attachmentName: string): Promise { + public async findByMostRecentVersionOfLearningObjectAndName( + hruid: string, + language: Language, + attachmentName: string + ): Promise { return this.findOne( { learningObject: { diff --git a/backend/src/data/content/learning-object-repository.ts b/backend/src/data/content/learning-object-repository.ts index 49b4c536..4684c6cc 100644 --- a/backend/src/data/content/learning-object-repository.ts +++ b/backend/src/data/content/learning-object-repository.ts @@ -5,7 +5,7 @@ import { Language } from '../../entities/content/language.js'; import { Teacher } from '../../entities/users/teacher.entity.js'; export class LearningObjectRepository extends DwengoEntityRepository { - public findByIdentifier(identifier: LearningObjectIdentifier): Promise { + public async findByIdentifier(identifier: LearningObjectIdentifier): Promise { return this.findOne( { hruid: identifier.hruid, @@ -18,7 +18,7 @@ export class LearningObjectRepository extends DwengoEntityRepository { return this.findOne( { hruid: hruid, @@ -33,7 +33,7 @@ export class LearningObjectRepository extends DwengoEntityRepository { + public async findAllByTeacher(teacher: Teacher): Promise { return this.find( { admins: teacher }, { populate: ['admins'] } // Make sure to load admin relations diff --git a/backend/src/data/content/learning-path-repository.ts b/backend/src/data/content/learning-path-repository.ts index a2f9b47e..e34508ec 100644 --- a/backend/src/data/content/learning-path-repository.ts +++ b/backend/src/data/content/learning-path-repository.ts @@ -3,7 +3,7 @@ import { LearningPath } from '../../entities/content/learning-path.entity.js'; import { Language } from '../../entities/content/language.js'; export class LearningPathRepository extends DwengoEntityRepository { - public findByHruidAndLanguage(hruid: string, language: Language): Promise { + public async findByHruidAndLanguage(hruid: string, language: Language): Promise { return this.findOne({ hruid: hruid, language: language }, { populate: ['nodes', 'nodes.transitions'] }); } diff --git a/backend/src/data/dwengo-entity-repository.ts b/backend/src/data/dwengo-entity-repository.ts index ce894e4b..1267c726 100644 --- a/backend/src/data/dwengo-entity-repository.ts +++ b/backend/src/data/dwengo-entity-repository.ts @@ -8,7 +8,7 @@ export abstract class DwengoEntityRepository extends EntityRep } await this.getEntityManager().persistAndFlush(entity); } - public async deleteWhere(query: FilterQuery) { + public async deleteWhere(query: FilterQuery): Promise { const toDelete = await this.findOne(query); const em = this.getEntityManager(); if (toDelete) { diff --git a/backend/src/data/questions/answer-repository.ts b/backend/src/data/questions/answer-repository.ts index a28342bd..a50bfd28 100644 --- a/backend/src/data/questions/answer-repository.ts +++ b/backend/src/data/questions/answer-repository.ts @@ -4,7 +4,7 @@ import { Question } from '../../entities/questions/question.entity.js'; import { Teacher } from '../../entities/users/teacher.entity.js'; export class AnswerRepository extends DwengoEntityRepository { - public createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { + public async createAnswer(answer: { toQuestion: Question; author: Teacher; content: string }): Promise { const answerEntity = this.create({ toQuestion: answer.toQuestion, author: answer.author, @@ -13,13 +13,13 @@ export class AnswerRepository extends DwengoEntityRepository { }); return this.insert(answerEntity); } - public findAllAnswersToQuestion(question: Question): Promise { + public async findAllAnswersToQuestion(question: Question): Promise { return this.findAll({ where: { toQuestion: question }, orderBy: { sequenceNumber: 'ASC' }, }); } - public removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { + public async removeAnswerByQuestionAndSequenceNumber(question: Question, sequenceNumber: number): Promise { return this.deleteWhere({ toQuestion: question, sequenceNumber: sequenceNumber, diff --git a/backend/src/data/questions/question-repository.ts b/backend/src/data/questions/question-repository.ts index deba7aad..8f5081ca 100644 --- a/backend/src/data/questions/question-repository.ts +++ b/backend/src/data/questions/question-repository.ts @@ -5,7 +5,7 @@ import { Student } from '../../entities/users/student.entity.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; export class QuestionRepository extends DwengoEntityRepository { - public createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { + public async createQuestion(question: { loId: LearningObjectIdentifier; author: Student; content: string }): Promise { const questionEntity = this.create({ learningObjectHruid: question.loId.hruid, learningObjectLanguage: question.loId.language, @@ -21,7 +21,7 @@ export class QuestionRepository extends DwengoEntityRepository { questionEntity.content = question.content; return this.insert(questionEntity); } - public findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { + public async findAllQuestionsAboutLearningObject(loId: LearningObjectIdentifier): Promise { return this.findAll({ where: { learningObjectHruid: loId.hruid, @@ -33,7 +33,7 @@ export class QuestionRepository extends DwengoEntityRepository { }, }); } - public removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise { + public async removeQuestionByLearningObjectAndSequenceNumber(loId: LearningObjectIdentifier, sequenceNumber: number): Promise { return this.deleteWhere({ learningObjectHruid: loId.hruid, learningObjectLanguage: loId.language, diff --git a/backend/src/data/repositories.ts b/backend/src/data/repositories.ts index cdeb50c1..f09c3c75 100644 --- a/backend/src/data/repositories.ts +++ b/backend/src/data/repositories.ts @@ -34,8 +34,8 @@ let entityManager: EntityManager | undefined; /** * Execute all the database operations within the function f in a single transaction. */ -export function transactional(f: () => Promise) { - entityManager?.transactional(f); +export async function transactional(f: () => Promise): Promise { + await entityManager?.transactional(f); } function repositoryGetter>(entity: EntityName): () => R { diff --git a/backend/src/data/users/student-repository.ts b/backend/src/data/users/student-repository.ts index a13fbb22..2efca048 100644 --- a/backend/src/data/users/student-repository.ts +++ b/backend/src/data/users/student-repository.ts @@ -1,14 +1,11 @@ import { Student } from '../../entities/users/student.entity.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; -// Import { UserRepository } from './user-repository.js'; - -// Export class StudentRepository extends UserRepository {} export class StudentRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username: username }); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username: username }); } } diff --git a/backend/src/data/users/teacher-repository.ts b/backend/src/data/users/teacher-repository.ts index 825b4d18..aa915627 100644 --- a/backend/src/data/users/teacher-repository.ts +++ b/backend/src/data/users/teacher-repository.ts @@ -2,10 +2,10 @@ import { Teacher } from '../../entities/users/teacher.entity.js'; import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; export class TeacherRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username: username }); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username: username }); } } diff --git a/backend/src/data/users/user-repository.ts b/backend/src/data/users/user-repository.ts index 21497b79..44eb0bc7 100644 --- a/backend/src/data/users/user-repository.ts +++ b/backend/src/data/users/user-repository.ts @@ -2,10 +2,10 @@ import { DwengoEntityRepository } from '../dwengo-entity-repository.js'; import { User } from '../../entities/users/user.entity.js'; export class UserRepository extends DwengoEntityRepository { - public findByUsername(username: string): Promise { + public async findByUsername(username: string): Promise { return this.findOne({ username } as Partial); } - public deleteByUsername(username: string): Promise { + public async deleteByUsername(username: string): Promise { return this.deleteWhere({ username } as Partial); } } diff --git a/backend/src/entities/assignments/submission.entity.ts b/backend/src/entities/assignments/submission.entity.ts index fbaa2791..e4330e0d 100644 --- a/backend/src/entities/assignments/submission.entity.ts +++ b/backend/src/entities/assignments/submission.entity.ts @@ -16,7 +16,7 @@ export class Submission { learningObjectLanguage!: Language; @PrimaryKey({ type: 'numeric' }) - learningObjectVersion: number = 1; + learningObjectVersion = 1; @PrimaryKey({ type: 'integer', autoincrement: true }) submissionNumber?: number; diff --git a/backend/src/entities/content/educational-goal.entity.ts b/backend/src/entities/content/educational-goal.entity.ts new file mode 100644 index 00000000..fafe1a01 --- /dev/null +++ b/backend/src/entities/content/educational-goal.entity.ts @@ -0,0 +1,10 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +@Embeddable() +export class EducationalGoal { + @Property({ type: 'string' }) + source!: string; + + @Property({ type: 'string' }) + id!: string; +} diff --git a/backend/src/entities/content/learning-object-identifier.ts b/backend/src/entities/content/learning-object-identifier.ts index 3c020bd7..9234afa7 100644 --- a/backend/src/entities/content/learning-object-identifier.ts +++ b/backend/src/entities/content/learning-object-identifier.ts @@ -5,5 +5,7 @@ export class LearningObjectIdentifier { public hruid: string, public language: Language, public version: number - ) {} + ) { + // Do nothing + } } diff --git a/backend/src/entities/content/learning-object.entity.ts b/backend/src/entities/content/learning-object.entity.ts index 9eda22ba..e352a10a 100644 --- a/backend/src/entities/content/learning-object.entity.ts +++ b/backend/src/entities/content/learning-object.entity.ts @@ -1,28 +1,12 @@ -import { Embeddable, Embedded, Entity, Enum, ManyToMany, OneToMany, PrimaryKey, Property } from '@mikro-orm/core'; +import { 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'; import { DwengoContentType } from '../../services/learning-objects/processing/content-type.js'; import { v4 } from 'uuid'; import { LearningObjectRepository } from '../../data/content/learning-object-repository.js'; - -@Embeddable() -export class EducationalGoal { - @Property({ type: 'string' }) - source!: string; - - @Property({ type: 'string' }) - id!: string; -} - -@Embeddable() -export class ReturnValue { - @Property({ type: 'string' }) - callbackUrl!: string; - - @Property({ type: 'json' }) - callbackSchema!: string; -} +import { EducationalGoal } from './educational-goal.entity.js'; +import { ReturnValue } from './return-value.entity.js'; @Entity({ repository: () => LearningObjectRepository }) export class LearningObject { @@ -36,7 +20,7 @@ export class LearningObject { language!: Language; @PrimaryKey({ type: 'number' }) - version: number = 1; + version = 1; @Property({ type: 'uuid', unique: true }) uuid = v4(); @@ -62,7 +46,7 @@ export class LearningObject { targetAges?: number[] = []; @Property({ type: 'bool' }) - teacherExclusive: boolean = false; + teacherExclusive = false; @Property({ type: 'array' }) skosConcepts: string[] = []; @@ -74,10 +58,10 @@ export class LearningObject { educationalGoals: EducationalGoal[] = []; @Property({ type: 'string' }) - copyright: string = ''; + copyright = ''; @Property({ type: 'string' }) - license: string = ''; + license = ''; @Property({ type: 'smallint', nullable: true }) difficulty?: number; @@ -91,7 +75,7 @@ export class LearningObject { returnValue!: ReturnValue; @Property({ type: 'bool' }) - available: boolean = true; + available = true; @Property({ type: 'string', nullable: true }) contentLocation?: string; diff --git a/backend/src/entities/content/return-value.entity.ts b/backend/src/entities/content/return-value.entity.ts new file mode 100644 index 00000000..d38b0693 --- /dev/null +++ b/backend/src/entities/content/return-value.entity.ts @@ -0,0 +1,10 @@ +import { Embeddable, Property } from '@mikro-orm/core'; + +@Embeddable() +export class ReturnValue { + @Property({ type: 'string' }) + callbackUrl!: string; + + @Property({ type: 'json' }) + callbackSchema!: string; +} diff --git a/backend/src/entities/questions/question.entity.ts b/backend/src/entities/questions/question.entity.ts index 058ba6b3..09e3cd46 100644 --- a/backend/src/entities/questions/question.entity.ts +++ b/backend/src/entities/questions/question.entity.ts @@ -15,7 +15,7 @@ export class Question { learningObjectLanguage!: Language; @PrimaryKey({ type: 'number' }) - learningObjectVersion: number = 1; + learningObjectVersion = 1; @PrimaryKey({ type: 'integer', autoincrement: true }) sequenceNumber?: number; diff --git a/backend/src/entities/users/user.entity.ts b/backend/src/entities/users/user.entity.ts index 1f35a0f8..15637110 100644 --- a/backend/src/entities/users/user.entity.ts +++ b/backend/src/entities/users/user.entity.ts @@ -6,8 +6,8 @@ export abstract class User { username!: string; @Property() - firstName: string = ''; + firstName = ''; @Property() - lastName: string = ''; + lastName = ''; } diff --git a/backend/src/exceptions/forbidden-exception.ts b/backend/src/exceptions/forbidden-exception.ts index 5712e0c8..4c58d1d5 100644 --- a/backend/src/exceptions/forbidden-exception.ts +++ b/backend/src/exceptions/forbidden-exception.ts @@ -6,7 +6,7 @@ import { ExceptionWithHttpState } from './exception-with-http-state.js'; export class ForbiddenException extends ExceptionWithHttpState { status = 403; - constructor(message: string = 'Forbidden') { + constructor(message = 'Forbidden') { super(403, message); } } diff --git a/backend/src/exceptions/unauthorized-exception.ts b/backend/src/exceptions/unauthorized-exception.ts index 7ea9aca8..54aa7cf9 100644 --- a/backend/src/exceptions/unauthorized-exception.ts +++ b/backend/src/exceptions/unauthorized-exception.ts @@ -4,7 +4,7 @@ import { ExceptionWithHttpState } from './exception-with-http-state.js'; * Exception for HTTP 401 Unauthorized */ export class UnauthorizedException extends ExceptionWithHttpState { - constructor(message: string = 'Unauthorized') { + constructor(message = 'Unauthorized') { super(401, message); } } diff --git a/backend/src/interfaces/assignment.ts b/backend/src/interfaces/assignment.ts index eefa8c96..698b5b40 100644 --- a/backend/src/interfaces/assignment.ts +++ b/backend/src/interfaces/assignment.ts @@ -3,6 +3,7 @@ import { Assignment } from '../entities/assignments/assignment.entity.js'; import { Class } from '../entities/classes/class.entity.js'; import { languageMap } from '../entities/content/language.js'; import { GroupDTO } from './group.js'; +import { getLogger } from '../logging/initalize.js'; export interface AssignmentDTO { id: number; @@ -46,5 +47,7 @@ export function mapToAssignment(assignmentData: AssignmentDTO, cls: Class): Assi assignment.learningPathLanguage = languageMap[assignmentData.language] || FALLBACK_LANG; assignment.within = cls; + getLogger().debug(assignment); + return assignment; } diff --git a/backend/src/interfaces/learning-content.ts b/backend/src/interfaces/learning-content.ts index 51474917..693aec37 100644 --- a/backend/src/interfaces/learning-content.ts +++ b/backend/src/interfaces/learning-content.ts @@ -58,7 +58,7 @@ export interface EducationalGoal { export interface ReturnValue { callback_url: string; - callback_schema: Record; + callback_schema: Record; } export interface LearningObjectMetadata { diff --git a/backend/src/logging/initalize.ts b/backend/src/logging/initalize.ts index 4d14e8ab..5c94a25f 100644 --- a/backend/src/logging/initalize.ts +++ b/backend/src/logging/initalize.ts @@ -1,7 +1,7 @@ import { createLogger, format, Logger as WinstonLogger, transports } from 'winston'; import LokiTransport from 'winston-loki'; import { LokiLabels } from 'loki-logger-ts'; -import { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; export class Logger extends WinstonLogger { constructor() { @@ -9,7 +9,7 @@ export class Logger extends WinstonLogger { } } -const Labels: LokiLabels = { +const lokiLabels: LokiLabels = { source: 'Dwengo-Backend', service: 'API', host: 'localhost', @@ -22,28 +22,28 @@ function initializeLogger(): Logger { return logger; } - const logLevel = getEnvVar(EnvVars.LogLevel); + const logLevel = getEnvVar(envVars.LogLevel); const consoleTransport = new transports.Console({ - level: getEnvVar(EnvVars.LogLevel), + level: getEnvVar(envVars.LogLevel), format: format.combine(format.cli(), format.colorize()), }); - if (getEnvVar(EnvVars.RunMode) === 'dev') { + if (getEnvVar(envVars.RunMode) === 'dev') { return createLogger({ transports: [consoleTransport], }); } - const lokiHost = getEnvVar(EnvVars.LokiHost); + const lokiHost = getEnvVar(envVars.LokiHost); const lokiTransport: LokiTransport = new LokiTransport({ host: lokiHost, - labels: Labels, + labels: lokiLabels, level: logLevel, json: true, format: format.combine(format.timestamp(), format.json()), - onConnectionError: (err) => { + onConnectionError: (err): void => { // eslint-disable-next-line no-console console.error(`Connection error: ${err}`); }, diff --git a/backend/src/logging/mikroOrmLogger.ts b/backend/src/logging/mikroOrmLogger.ts index 25bbac13..9cb797a8 100644 --- a/backend/src/logging/mikroOrmLogger.ts +++ b/backend/src/logging/mikroOrmLogger.ts @@ -5,35 +5,54 @@ import { LokiLabels } from 'loki-logger-ts'; export class MikroOrmLogger extends DefaultLogger { private logger: Logger = getLogger(); - log(namespace: LoggerNamespace, message: string, context?: LogContext) { + static createMessage(namespace: LoggerNamespace, messageArg: string, context?: LogContext): unknown { + 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, + }; + } + + log(namespace: LoggerNamespace, message: string, context?: LogContext): void { if (!this.isEnabled(namespace, context)) { return; } switch (namespace) { case 'query': - this.logger.debug(this.createMessage(namespace, message, context)); + this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'query-params': // TODO Which log level should this be? - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'schema': - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'discovery': - this.logger.debug(this.createMessage(namespace, message, context)); + this.logger.debug(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'info': - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'deprecated': - this.logger.warn(this.createMessage(namespace, message, context)); + this.logger.warn(MikroOrmLogger.createMessage(namespace, message, context)); break; default: switch (context?.level) { case 'info': - this.logger.info(this.createMessage(namespace, message, context)); + this.logger.info(MikroOrmLogger.createMessage(namespace, message, context)); break; case 'warning': this.logger.warn(message); @@ -47,23 +66,4 @@ export class MikroOrmLogger extends DefaultLogger { } } } - - 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, - }; - } } diff --git a/backend/src/logging/responseTimeLogger.ts b/backend/src/logging/responseTimeLogger.ts index c1bb1e33..7fcc6c93 100644 --- a/backend/src/logging/responseTimeLogger.ts +++ b/backend/src/logging/responseTimeLogger.ts @@ -1,7 +1,7 @@ import { getLogger, Logger } from './initalize.js'; import { Request, Response } from 'express'; -export function responseTimeLogger(req: Request, res: Response, time: number) { +export function responseTimeLogger(req: Request, res: Response, time: number): void { const logger: Logger = getLogger(); const method = req.method; diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 4e651b4b..a91932ea 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -1,9 +1,9 @@ -import { EnvVars, getEnvVar } from '../../util/envvars.js'; +import { envVars, getEnvVar } from '../../util/envVars.js'; import { expressjwt } from 'express-jwt'; +import * as jwt from 'jsonwebtoken'; 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 { UnauthorizedException } from '../../exceptions/unauthorized-exception.js'; @@ -33,12 +33,12 @@ function createJwksClient(uri: string): jwksClient.JwksClient { const idpConfigs = { student: { - issuer: getEnvVar(EnvVars.IdpStudentUrl), - jwksClient: createJwksClient(getEnvVar(EnvVars.IdpStudentJwksEndpoint)), + issuer: getEnvVar(envVars.IdpStudentUrl), + jwksClient: createJwksClient(getEnvVar(envVars.IdpStudentJwksEndpoint)), }, teacher: { - issuer: getEnvVar(EnvVars.IdpTeacherUrl), - jwksClient: createJwksClient(getEnvVar(EnvVars.IdpTeacherJwksEndpoint)), + issuer: getEnvVar(envVars.IdpTeacherUrl), + jwksClient: createJwksClient(getEnvVar(envVars.IdpTeacherJwksEndpoint)), }, }; @@ -64,7 +64,7 @@ const verifyJwtToken = expressjwt({ } return signingKey.getPublicKey(); }, - audience: getEnvVar(EnvVars.IdpAudience), + audience: getEnvVar(envVars.IdpAudience), algorithms: [JWT_ALGORITHM], credentialsRequired: false, requestProperty: REQUEST_PROPERTY_FOR_JWT_PAYLOAD, @@ -75,7 +75,7 @@ const verifyJwtToken = expressjwt({ */ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | undefined { if (!req.jwtPayload) { - return; + return undefined; } const issuer = req.jwtPayload.iss; let accountType: 'student' | 'teacher'; @@ -85,8 +85,9 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | } else if (issuer === idpConfigs.teacher.issuer) { accountType = 'teacher'; } else { - return; + return undefined; } + return { accountType: accountType, username: req.jwtPayload[JWT_PROPERTY_NAMES.username]!, @@ -101,10 +102,10 @@ function getAuthenticationInfo(req: AuthenticatedRequest): AuthenticationInfo | * 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) => { +function addAuthenticationInfo(req: AuthenticatedRequest, _res: express.Response, next: express.NextFunction): void { req.auth = getAuthenticationInfo(req); next(); -}; +} export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; @@ -114,9 +115,8 @@ export const authenticateUser = [verifyJwtToken, addAuthenticationInfo]; * @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 => { +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)) { @@ -125,6 +125,7 @@ export const authorize = next(); } }; +} /** * Middleware which rejects all unauthenticated users, but accepts all authenticated users. diff --git a/backend/src/middleware/auth/authentication-info.d.ts b/backend/src/middleware/auth/authentication-info.d.ts index 4b060dfa..e8f0d48c 100644 --- a/backend/src/middleware/auth/authentication-info.d.ts +++ b/backend/src/middleware/auth/authentication-info.d.ts @@ -1,11 +1,11 @@ /** * Object with information about the user who is currently logged in. */ -export type AuthenticationInfo = { +export interface AuthenticationInfo { accountType: 'student' | 'teacher'; username: string; name?: string; firstName?: string; lastName?: string; email?: string; -}; +} diff --git a/backend/src/middleware/cors.ts b/backend/src/middleware/cors.ts index 3d2c9be0..48e0704d 100644 --- a/backend/src/middleware/cors.ts +++ b/backend/src/middleware/cors.ts @@ -1,7 +1,7 @@ import cors from 'cors'; -import { EnvVars, getEnvVar } from '../util/envvars.js'; +import { envVars, getEnvVar } from '../util/envVars.js'; export default cors({ - origin: getEnvVar(EnvVars.CorsAllowedOrigins).split(','), - allowedHeaders: getEnvVar(EnvVars.CorsAllowedHeaders).split(','), + origin: getEnvVar(envVars.CorsAllowedOrigins).split(','), + allowedHeaders: getEnvVar(envVars.CorsAllowedHeaders).split(','), }); diff --git a/backend/src/middleware/error-handling/error-handler.ts b/backend/src/middleware/error-handling/error-handler.ts index f2806938..d7315603 100644 --- a/backend/src/middleware/error-handling/error-handler.ts +++ b/backend/src/middleware/error-handling/error-handler.ts @@ -4,7 +4,7 @@ import { ExceptionWithHttpState } from '../../exceptions/exception-with-http-sta const logger: Logger = getLogger(); -export function errorHandler(err: unknown, req: Request, res: Response, _: NextFunction): void { +export function errorHandler(err: unknown, _req: Request, res: Response, _: NextFunction): void { if (err instanceof ExceptionWithHttpState) { logger.warn(`An error occurred while handling a request: ${err} (-> HTTP ${err.status})`); res.status(err.status).json(err); diff --git a/backend/src/mikro-orm.config.ts b/backend/src/mikro-orm.config.ts index 9e7800b0..eb0e5f7a 100644 --- a/backend/src/mikro-orm.config.ts +++ b/backend/src/mikro-orm.config.ts @@ -1,6 +1,6 @@ import { LoggerOptions, Options } from '@mikro-orm/core'; import { PostgreSqlDriver } from '@mikro-orm/postgresql'; -import { EnvVars, getEnvVar, getNumericEnvVar } from './util/envvars.js'; +import { envVars, getEnvVar, getNumericEnvVar } from './util/envVars.js'; import { SqliteDriver } from '@mikro-orm/sqlite'; import { MikroOrmLogger } from './logging/mikroOrmLogger.js'; @@ -42,11 +42,11 @@ const entities = [ Question, ]; -function config(testingMode: boolean = false): Options { +function config(testingMode = false): Options { if (testingMode) { return { driver: SqliteDriver, - dbName: getEnvVar(EnvVars.DbName), + dbName: getEnvVar(envVars.DbName), subscribers: [new SqliteAutoincrementSubscriber()], entities: entities, persistOnCreate: false, // Do not implicitly save entities when they are created via `create`. @@ -54,23 +54,23 @@ function config(testingMode: boolean = false): Options { // 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), + dynamicImportProvider: async (id) => import(id), }; } return { driver: PostgreSqlDriver, - host: getEnvVar(EnvVars.DbHost), - port: getNumericEnvVar(EnvVars.DbPort), - dbName: getEnvVar(EnvVars.DbName), - user: getEnvVar(EnvVars.DbUsername), - password: getEnvVar(EnvVars.DbPassword), + host: getEnvVar(envVars.DbHost), + port: getNumericEnvVar(envVars.DbPort), + dbName: getEnvVar(envVars.DbName), + user: getEnvVar(envVars.DbUsername), + password: getEnvVar(envVars.DbPassword), entities: entities, persistOnCreate: false, // Do not implicitly save entities when they are created via `create`. // EntitiesTs: entitiesTs, // Logging - debug: getEnvVar(EnvVars.LogLevel) === 'debug', + debug: getEnvVar(envVars.LogLevel) === 'debug', loggerFactory: (options: LoggerOptions) => new MikroOrmLogger(options), }; } diff --git a/backend/src/orm.ts b/backend/src/orm.ts index 93feea7a..3e6e26c8 100644 --- a/backend/src/orm.ts +++ b/backend/src/orm.ts @@ -1,10 +1,10 @@ import { EntityManager, MikroORM } from '@mikro-orm/core'; import config from './mikro-orm.config.js'; -import { EnvVars, getEnvVar } from './util/envvars.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) { +export async function initORM(testingMode = false): Promise { const logger: Logger = getLogger(); logger.info('Initializing ORM'); @@ -12,7 +12,7 @@ export async function initORM(testingMode: boolean = false) { orm = await MikroORM.init(config(testingMode)); // Update the database scheme if necessary and enabled. - if (getEnvVar(EnvVars.DbUpdate)) { + if (getEnvVar(envVars.DbUpdate)) { await orm.schema.updateSchema(); } else { const diff = await orm.schema.getUpdateSchemaSQL(); diff --git a/backend/src/routes/assignments.ts b/backend/src/routes/assignments.ts index a733d093..3652dcc6 100644 --- a/backend/src/routes/assignments.ts +++ b/backend/src/routes/assignments.ts @@ -19,7 +19,7 @@ router.get('/:id', getAssignmentHandler); router.get('/:id/submissions', getAssignmentsSubmissionsHandler); -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/auth.ts b/backend/src/routes/auth.ts index 778e51fd..4a1f27d2 100644 --- a/backend/src/routes/auth.ts +++ b/backend/src/routes/auth.ts @@ -4,21 +4,21 @@ import { authenticatedOnly, studentsOnly, teachersOnly } from '../middleware/aut const router = express.Router(); // Returns auth configuration for frontend -router.get('/config', (req, res) => { +router.get('/config', (_req, res) => { res.json(getFrontendAuthConfig()); }); -router.get('/testAuthenticatedOnly', authenticatedOnly, (req, res) => { +router.get('/testAuthenticatedOnly', authenticatedOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }, { "teacher": [ ] }] */ res.json({ message: 'If you see this, you should be authenticated!' }); }); -router.get('/testStudentsOnly', studentsOnly, (req, res) => { +router.get('/testStudentsOnly', studentsOnly, (_req, res) => { /* #swagger.security = [{ "student": [ ] }] */ res.json({ message: 'If you see this, you should be a student!' }); }); -router.get('/testTeachersOnly', teachersOnly, (req, res) => { +router.get('/testTeachersOnly', teachersOnly, (_req, res) => { /* #swagger.security = [{ "teacher": [ ] }] */ res.json({ message: 'If you see this, you should be a teacher!' }); }); diff --git a/backend/src/routes/groups.ts b/backend/src/routes/groups.ts index 0c9692b0..dc8917bd 100644 --- a/backend/src/routes/groups.ts +++ b/backend/src/routes/groups.ts @@ -14,7 +14,7 @@ router.get('/:groupid', getGroupHandler); router.get('/:groupid', getGroupSubmissionsHandler); // The list of questions a group has made -router.get('/:id/questions', (req, res) => { +router.get('/:id/questions', (_req, res) => { res.json({ questions: ['0'], }); diff --git a/backend/src/routes/submissions.ts b/backend/src/routes/submissions.ts index 4db93027..8e9831b9 100644 --- a/backend/src/routes/submissions.ts +++ b/backend/src/routes/submissions.ts @@ -3,7 +3,7 @@ import { createSubmissionHandler, deleteSubmissionHandler, getSubmissionHandler const router = express.Router({ mergeParams: true }); // Root endpoint used to search objects -router.get('/', (req, res) => { +router.get('/', (_req, res) => { res.json({ submissions: ['0', '1'], }); diff --git a/backend/src/routes/teachers.ts b/backend/src/routes/teachers.ts index 746e0d16..a6106a80 100644 --- a/backend/src/routes/teachers.ts +++ b/backend/src/routes/teachers.ts @@ -32,7 +32,7 @@ router.get('/:username/joinRequests/:classId', getStudentJoinRequestHandler); router.put('/:username/joinRequests/:classId/:studentUsername', updateStudentJoinRequestHandler); // Invitations to other classes a teacher received -router.get('/:id/invitations', (req, res) => { +router.get('/:id/invitations', (_req, res) => { res.json({ invitations: ['0'], }); diff --git a/backend/src/services/assignments.ts b/backend/src/services/assignments.ts index a21a96fa..22c5ce9e 100644 --- a/backend/src/services/assignments.ts +++ b/backend/src/services/assignments.ts @@ -1,6 +1,7 @@ import { getAssignmentRepository, getClassRepository, getGroupRepository, getSubmissionRepository } from '../data/repositories.js'; import { AssignmentDTO, mapToAssignment, mapToAssignmentDTO, mapToAssignmentDTOId } from '../interfaces/assignment.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; +import { getLogger } from '../logging/initalize.js'; export async function getAllAssignments(classid: string, full: boolean): Promise { const classRepository = getClassRepository(); @@ -37,7 +38,7 @@ export async function createAssignment(classid: string, assignmentData: Assignme return mapToAssignmentDTO(newAssignment); } catch (e) { - console.error(e); + getLogger().error(e); return null; } } @@ -83,7 +84,7 @@ export async function getAssignmentsSubmissions( const groups = await groupRepository.findAllGroupsForAssignment(assignment); const submissionRepository = getSubmissionRepository(); - const submissions = (await Promise.all(groups.map((group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); + const submissions = (await Promise.all(groups.map(async (group) => submissionRepository.findAllSubmissionsForGroup(group)))).flat(); if (full) { return submissions.map(mapToSubmissionDTO); diff --git a/backend/src/services/classes.ts b/backend/src/services/classes.ts index 56d801f3..7e773fbf 100644 --- a/backend/src/services/classes.ts +++ b/backend/src/services/classes.ts @@ -36,11 +36,15 @@ export async function getAllClasses(full: boolean): Promise { const teacherRepository = getTeacherRepository(); const teacherUsernames = classData.teachers || []; - const teachers = (await Promise.all(teacherUsernames.map((id) => teacherRepository.findByUsername(id)))).filter((teacher) => teacher !== null); + const teachers = (await Promise.all(teacherUsernames.map(async (id) => teacherRepository.findByUsername(id)))).filter( + (teacher) => teacher !== null + ); const studentRepository = getStudentRepository(); const studentUsernames = classData.students || []; - const students = (await Promise.all(studentUsernames.map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); + const students = (await Promise.all(studentUsernames.map(async (id) => studentRepository.findByUsername(id)))).filter( + (student) => student !== null + ); const classRepository = getClassRepository(); diff --git a/backend/src/services/groups.ts b/backend/src/services/groups.ts index 4a1cbbf0..16895e0a 100644 --- a/backend/src/services/groups.ts +++ b/backend/src/services/groups.ts @@ -8,6 +8,7 @@ import { import { Group } from '../entities/assignments/group.entity.js'; import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; +import { getLogger } from '../logging/initalize.js'; export async function getGroup(classId: string, assignmentNumber: number, groupNumber: number, full: boolean): Promise { const classRepository = getClassRepository(); @@ -42,9 +43,11 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme const studentRepository = getStudentRepository(); const memberUsernames = (groupData.members as string[]) || []; // TODO check if groupdata.members is a list - const members = (await Promise.all([...memberUsernames].map((id) => studentRepository.findByUsername(id)))).filter((student) => student !== null); + const members = (await Promise.all([...memberUsernames].map(async (id) => studentRepository.findByUsername(id)))).filter( + (student) => student !== null + ); - console.log(members); + getLogger().debug(members); const classRepository = getClassRepository(); const cls = await classRepository.findById(classid); @@ -70,7 +73,7 @@ export async function createGroup(groupData: GroupDTO, classid: string, assignme return newGroup; } catch (e) { - console.log(e); + getLogger().error(e); return null; } } @@ -94,8 +97,7 @@ export async function getAllGroups(classId: string, assignmentNumber: number, fu const groups = await groupRepository.findAllGroupsForAssignment(assignment); if (full) { - console.log('full'); - console.log(groups); + getLogger().debug({ full: full, groups: groups }); return groups.map(mapToGroupDTO); } diff --git a/backend/src/services/learning-objects.ts b/backend/src/services/learning-objects.ts index faa77cb4..43af1aca 100644 --- a/backend/src/services/learning-objects.ts +++ b/backend/src/services/learning-objects.ts @@ -1,6 +1,7 @@ import { DWENGO_API_BASE } from '../config.js'; import { fetchWithLogging } from '../util/api-helper.js'; import { FilteredLearningObject, LearningObjectMetadata, LearningObjectNode, LearningPathResponse } from '../interfaces/learning-content.js'; +import { getLogger } from '../logging/initalize.js'; function filterData(data: LearningObjectMetadata, htmlUrl: string): FilteredLearningObject { return { @@ -37,7 +38,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr ); if (!metadata) { - console.error(`⚠️ WARNING: Learning object "${hruid}" not found.`); + getLogger().error(`⚠️ WARNING: Learning object "${hruid}" not found.`); return null; } @@ -48,7 +49,7 @@ export async function getLearningObjectById(hruid: string, language: string): Pr /** * Generic function to fetch learning paths */ -function fetchLearningPaths(arg0: string[], language: string, arg2: string): LearningPathResponse | PromiseLike { +function fetchLearningPaths(_arg0: string[], _language: string, _arg2: string): LearningPathResponse | PromiseLike { throw new Error('Function not implemented.'); } @@ -60,7 +61,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri const learningPathResponse: LearningPathResponse = await fetchLearningPaths([hruid], language, `Learning path for HRUID "${hruid}"`); if (!learningPathResponse.success || !learningPathResponse.data?.length) { - console.error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); + getLogger().error(`⚠️ WARNING: Learning path "${hruid}" exists but contains no learning objects.`); return []; } @@ -74,7 +75,7 @@ async function fetchLearningObjects(hruid: string, full: boolean, language: stri objects.filter((obj): obj is FilteredLearningObject => obj !== null) ); } catch (error) { - console.error('❌ Error fetching learning objects:', error); + getLogger().error('❌ Error fetching learning objects:', error); return []; } } diff --git a/backend/src/services/learning-objects/attachment-service.ts b/backend/src/services/learning-objects/attachment-service.ts index aacc7187..4ff4ec47 100644 --- a/backend/src/services/learning-objects/attachment-service.ts +++ b/backend/src/services/learning-objects/attachment-service.ts @@ -3,7 +3,7 @@ import { Attachment } from '../../entities/content/attachment.entity.js'; import { LearningObjectIdentifier } from '../../interfaces/learning-content.js'; const attachmentService = { - getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { + async getAttachment(learningObjectId: LearningObjectIdentifier, attachmentName: string): Promise { const attachmentRepo = getAttachmentRepository(); if (learningObjectId.version) { diff --git a/backend/src/services/learning-objects/database-learning-object-provider.ts b/backend/src/services/learning-objects/database-learning-object-provider.ts index bab0b9b1..a8055f2c 100644 --- a/backend/src/services/learning-objects/database-learning-object-provider.ts +++ b/backend/src/services/learning-objects/database-learning-object-provider.ts @@ -1,7 +1,6 @@ import { LearningObjectProvider } from './learning-object-provider.js'; import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import { getLearningObjectRepository, getLearningPathRepository } from '../../data/repositories.js'; -import { Language } from '../../entities/content/language.js'; import { LearningObject } from '../../entities/content/learning-object.entity.js'; import { getUrlStringForLearningObject } from '../../util/links.js'; import processingService from './processing/processing-service.js'; @@ -41,10 +40,10 @@ function convertLearningObject(learningObject: LearningObject | null): FilteredL }; } -function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { +async function findLearningObjectEntityById(id: LearningObjectIdentifier): Promise { const learningObjectRepo = getLearningObjectRepository(); - return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); + return learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); } /** @@ -65,11 +64,11 @@ const databaseLearningObjectProvider: LearningObjectProvider = { async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { const learningObjectRepo = getLearningObjectRepository(); - const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language as Language); + const learningObject = await learningObjectRepo.findLatestByHruidAndLanguage(id.hruid, id.language); if (!learningObject) { return null; } - return await processingService.render(learningObject, (id) => findLearningObjectEntityById(id)); + return await processingService.render(learningObject, async (id) => findLearningObjectEntityById(id)); }, /** @@ -96,7 +95,7 @@ const databaseLearningObjectProvider: LearningObjectProvider = { throw new NotFoundError('The learning path with the given ID could not be found.'); } const learningObjects = await Promise.all( - learningPath.nodes.map((it) => { + learningPath.nodes.map(async (it) => { const learningObject = learningObjectService.getLearningObjectById({ hruid: it.learningObjectHruid, language: it.language, diff --git a/backend/src/services/learning-objects/learning-object-service.ts b/backend/src/services/learning-objects/learning-object-service.ts index 8289660b..59ffb643 100644 --- a/backend/src/services/learning-objects/learning-object-service.ts +++ b/backend/src/services/learning-objects/learning-object-service.ts @@ -1,11 +1,11 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifier } from '../../interfaces/learning-content.js'; import dwengoApiLearningObjectProvider from './dwengo-api-learning-object-provider.js'; import { LearningObjectProvider } from './learning-object-provider.js'; -import { EnvVars, getEnvVar } from '../../util/envvars.js'; +import { envVars, getEnvVar } from '../../util/envVars.js'; import databaseLearningObjectProvider from './database-learning-object-provider.js'; function getProvider(id: LearningObjectIdentifier): LearningObjectProvider { - if (id.hruid.startsWith(getEnvVar(EnvVars.UserContentPrefix))) { + if (id.hruid.startsWith(getEnvVar(envVars.UserContentPrefix))) { return databaseLearningObjectProvider; } return dwengoApiLearningObjectProvider; @@ -18,28 +18,28 @@ const learningObjectService = { /** * Fetches a single learning object by its HRUID */ - getLearningObjectById(id: LearningObjectIdentifier): Promise { + async getLearningObjectById(id: LearningObjectIdentifier): Promise { return getProvider(id).getLearningObjectById(id); }, /** * Fetch full learning object data (metadata) */ - getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { + async getLearningObjectsFromPath(id: LearningPathIdentifier): Promise { return getProvider(id).getLearningObjectsFromPath(id); }, /** * Fetch only learning object HRUIDs */ - getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { + async getLearningObjectIdsFromPath(id: LearningPathIdentifier): Promise { return getProvider(id).getLearningObjectIdsFromPath(id); }, /** * Obtain a HTML-rendering of the learning object with the given identifier (as a string). */ - getLearningObjectHTML(id: LearningObjectIdentifier): Promise { + async getLearningObjectHTML(id: LearningObjectIdentifier): Promise { return getProvider(id).getLearningObjectHTML(id); }, }; diff --git a/backend/src/services/learning-objects/processing/audio/audio-processor.ts b/backend/src/services/learning-objects/processing/audio/audio-processor.ts index 592669d5..227eae13 100644 --- a/backend/src/services/learning-objects/processing/audio/audio-processor.ts +++ b/backend/src/services/learning-objects/processing/audio/audio-processor.ts @@ -14,7 +14,7 @@ class AudioProcessor extends StringProcessor { super(DwengoContentType.AUDIO_MPEG); } - protected renderFn(audioUrl: string): string { + override renderFn(audioUrl: string): string { return DOMPurify.sanitize(`