diff --git a/backend/src/app.ts b/backend/src/app.ts index 0c5e8892..3ee7307b 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -9,6 +9,7 @@ import { EnvVars, getNumericEnvVar } from './util/envvars.js'; import apiRouter from './routes/router.js'; import swaggerMiddleware from './swagger.js'; import swaggerUi from 'swagger-ui-express'; +import {errorHandler} from "./middleware/error-handling/error-handler"; const logger: Logger = getLogger(); @@ -26,6 +27,8 @@ app.use('/api', apiRouter); // Swagger app.use('/api-docs', swaggerUi.serve, swaggerMiddleware); +app.use(errorHandler); + async function startServer() { await initORM(); diff --git a/backend/src/controllers/learning-objects.ts b/backend/src/controllers/learning-objects.ts index e9d0d727..8f5b18b9 100644 --- a/backend/src/controllers/learning-objects.ts +++ b/backend/src/controllers/learning-objects.ts @@ -4,9 +4,9 @@ import { FilteredLearningObject, LearningObjectIdentifier, LearningPathIdentifie import learningObjectService from '../services/learning-objects/learning-object-service.js'; import { EnvVars, getEnvVar } from '../util/envvars.js'; import { Language } from '../entities/content/language.js'; -import { BadRequestException } from '../exceptions.js'; import attachmentService from '../services/learning-objects/attachment-service.js'; import { NotFoundError } from '@mikro-orm/core'; +import {BadRequestException} from "../exceptions/badRequestException"; function getLearningObjectIdentifierFromRequest(req: Request): LearningObjectIdentifier { if (!req.params.hruid) { diff --git a/backend/src/controllers/learning-paths.ts b/backend/src/controllers/learning-paths.ts index 37f92d91..bf6f94d7 100644 --- a/backend/src/controllers/learning-paths.ts +++ b/backend/src/controllers/learning-paths.ts @@ -2,13 +2,14 @@ import { Request, Response } from 'express'; import { themes } from '../data/themes.js'; import { FALLBACK_LANG } from '../config.js'; import learningPathService from '../services/learning-paths/learning-path-service.js'; -import { BadRequestException, NotFoundException } from '../exceptions.js'; import { Language } from '../entities/content/language.js'; import { PersonalizationTarget, personalizedForGroup, personalizedForStudent, } from '../services/learning-paths/learning-path-personalization-util.js'; +import {BadRequestException} from "../exceptions/badRequestException"; +import {NotFoundException} from "../exceptions/not-found-exception"; /** * Fetch learning paths based on query parameters. diff --git a/backend/src/exceptions.ts b/backend/src/exceptions.ts deleted file mode 100644 index e93a6c93..00000000 --- a/backend/src/exceptions.ts +++ /dev/null @@ -1,42 +0,0 @@ -/** - * Exception for HTTP 400 Bad Request - */ -export class BadRequestException extends Error { - public status = 400; - - constructor(error: string) { - super(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 - */ -export class NotFoundException extends Error { - public status = 404; - - constructor(error: string) { - super(error); - } -} diff --git a/backend/src/exceptions/bad-request-exception.ts b/backend/src/exceptions/bad-request-exception.ts new file mode 100644 index 00000000..eb5d4db0 --- /dev/null +++ b/backend/src/exceptions/bad-request-exception.ts @@ -0,0 +1,10 @@ +import {ExceptionWithHttpState} from "./exception-with-http-state.js"; + +/** + * Exception for HTTP 400 Bad Request + */ +export abstract class BadRequestException extends ExceptionWithHttpState { + constructor(error: string) { + super(400, error); + } +} diff --git a/backend/src/exceptions/conflict-exception.ts b/backend/src/exceptions/conflict-exception.ts new file mode 100644 index 00000000..b69081cb --- /dev/null +++ b/backend/src/exceptions/conflict-exception.ts @@ -0,0 +1,12 @@ +import {ExceptionWithHttpState} from "./exception-with-http-state.js"; + +/** + * Exception for HTTP 409 Conflict + */ +export class ConflictException extends ExceptionWithHttpState { + public status = 409; + + constructor(error: string) { + super(409, error); + } +} diff --git a/backend/src/exceptions/exception-with-http-state.ts b/backend/src/exceptions/exception-with-http-state.ts new file mode 100644 index 00000000..fab6b1dd --- /dev/null +++ b/backend/src/exceptions/exception-with-http-state.ts @@ -0,0 +1,9 @@ +/** + * Exceptions which are associated with a HTTP error code. + */ +export abstract class ExceptionWithHttpState extends Error { + constructor(public status: number, public error: string) { + super(error); + } +} + diff --git a/backend/src/exceptions/forbidden-exception.ts b/backend/src/exceptions/forbidden-exception.ts new file mode 100644 index 00000000..ba084162 --- /dev/null +++ b/backend/src/exceptions/forbidden-exception.ts @@ -0,0 +1,12 @@ +import {ExceptionWithHttpState} from "./exception-with-http-state.js"; + +/** + * Exception for HTTP 403 Forbidden + */ +export class ForbiddenException extends ExceptionWithHttpState { + status = 403; + + constructor(message: string = 'Forbidden') { + super(403, message); + } +} diff --git a/backend/src/exceptions/not-found-exception.ts b/backend/src/exceptions/not-found-exception.ts new file mode 100644 index 00000000..8fb2f3f1 --- /dev/null +++ b/backend/src/exceptions/not-found-exception.ts @@ -0,0 +1,12 @@ +import {ExceptionWithHttpState} from "./exception-with-http-state.js"; + +/** + * Exception for HTTP 404 Not Found + */ +export class NotFoundException extends ExceptionWithHttpState { + public status = 404; + + constructor(error: string) { + super(404, error); + } +} diff --git a/backend/src/exceptions/unauthorized-exception.ts b/backend/src/exceptions/unauthorized-exception.ts new file mode 100644 index 00000000..1c61a742 --- /dev/null +++ b/backend/src/exceptions/unauthorized-exception.ts @@ -0,0 +1,10 @@ +import {ExceptionWithHttpState} from "./exception-with-http-state.js"; + +/** + * Exception for HTTP 401 Unauthorized + */ +export class UnauthorizedException extends ExceptionWithHttpState { + constructor(message: string = 'Unauthorized') { + super(401, message); + } +} diff --git a/backend/src/middleware/auth/auth.ts b/backend/src/middleware/auth/auth.ts index 5ff5a53c..b98fb262 100644 --- a/backend/src/middleware/auth/auth.ts +++ b/backend/src/middleware/auth/auth.ts @@ -6,7 +6,8 @@ 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.js'; +import {UnauthorizedException} from "../../exceptions/unauthorized-exception"; +import {ForbiddenException} from "../../exceptions/forbidden-exception"; const JWKS_CACHE = true; const JWKS_RATE_LIMIT = true; diff --git a/backend/src/middleware/error-handling/error-handler.ts b/backend/src/middleware/error-handling/error-handler.ts new file mode 100644 index 00000000..0c91f511 --- /dev/null +++ b/backend/src/middleware/error-handling/error-handler.ts @@ -0,0 +1,15 @@ +import {NextFunction, Request, Response} from "express"; +import {getLogger, Logger} from "../../logging/initalize"; +import {ExceptionWithHttpState} from "../../exceptions/exception-with-http-state"; + +const logger: Logger = getLogger(); + +export function errorHandler(err: unknown, req: Request, res: Response, _: NextFunction): void { + if (err instanceof ExceptionWithHttpState) { + logger.warn(`An error occurred while handling request ${JSON.stringify(req)}: ${err.error} (-> HTTP ${err.status})`); + res.status(err.status).json(err); + } else { + logger.error(`Unexpected error occurred while handling request ${JSON.stringify(req)}: ${JSON.stringify(err)}`); + res.status(500).json(err); + } +} diff --git a/backend/src/services/students.ts b/backend/src/services/students.ts index 97e41427..da73f8ad 100644 --- a/backend/src/services/students.ts +++ b/backend/src/services/students.ts @@ -5,6 +5,9 @@ import { GroupDTO, mapToGroupDTO, mapToGroupDTOId } from '../interfaces/group.js import { mapToStudent, mapToStudentDTO, StudentDTO } from '../interfaces/student.js'; import { mapToSubmissionDTO, mapToSubmissionDTOId, SubmissionDTO, SubmissionDTOId } from '../interfaces/submission.js'; import { getAllAssignments } from './assignments.js'; +import {UniqueConstraintViolationException} from "@mikro-orm/core"; + +import {ConflictException} from "../exceptions/conflict-exception"; export async function getAllStudents(full: boolean): Promise { const studentRepository = getStudentRepository(); @@ -29,11 +32,12 @@ export async function createStudent(userData: StudentDTO): Promise { const teacherRepository = getTeacherRepository(); @@ -40,8 +39,10 @@ export async function createTeacher(userData: TeacherDTO): Promise