diff --git a/src/app.ts b/src/app.ts index c5c7056e..11a01db4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -9,7 +9,8 @@ import staffRouter from "./services/staff/staff-router.js"; import newsletterRouter from "./services/newsletter/newsletter-router.js"; import versionRouter from "./services/version/version-router.js"; import admissionRouter from "./services/admission/admission-router.js"; - +// import { InitializeConfigReader } from "./middleware/config-reader.js"; +import { ErrorHandler } from "./middleware/error-handler.js"; import Models from "./database/models.js"; import { StatusCode } from "status-code-enum"; import Config from "./config.js"; @@ -52,6 +53,8 @@ export function setupServer(): void { Models.initialize(); } +app.use(ErrorHandler); + export function startServer(): Promise { // eslint-disable-next-line no-magic-numbers const port = Config.PORT; diff --git a/src/middleware/error-handler.ts b/src/middleware/error-handler.ts new file mode 100644 index 00000000..cfcd2da3 --- /dev/null +++ b/src/middleware/error-handler.ts @@ -0,0 +1,48 @@ +import { Request, Response, NextFunction } from "express"; +import { StatusCode } from "status-code-enum"; + +export class RouterError { + statusCode: number; + message: string; + // NOTE: eslint is required because the goal of RouterError.data is to "return any necessary data" - mostly used for debugging purposes + // eslint-disable-next-line @typescript-eslint/no-explicit-any + data?: any | undefined; + catchErrorMessage?: string | undefined; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + constructor(statusCode?: number, message?: string, data?: any, catchErrorMessage?: string) { + this.statusCode = statusCode ?? StatusCode.ServerErrorInternal; + this.message = message ?? "Internal Server Error"; + this.data = data; + if (catchErrorMessage) { + this.catchErrorMessage = catchErrorMessage; + console.error(catchErrorMessage); + } else { + this.catchErrorMessage = ""; + } + } +} + +// _next is intentionally not used in this middleware +// eslint-disable-next-line @typescript-eslint/no-unused-vars +export function ErrorHandler(error: RouterError, _req: Request, resp: Response, _next: NextFunction): Response { + const statusCode: number = error.statusCode; + const message: string = error.message; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const data: any | undefined = error.data; + const catchErrorMessage: string | undefined = error.catchErrorMessage; + + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const jsonData: { [key: string]: any } = { + success: statusCode === StatusCode.SuccessOK, + error: message, + }; + if (data) { + jsonData["data"] = data; + } + if (catchErrorMessage) { + jsonData["error_message"] = catchErrorMessage; + } + + return resp.status(statusCode).json(jsonData); +} diff --git a/src/services/admission/admission-router.ts b/src/services/admission/admission-router.ts index 29013530..273fc6f1 100644 --- a/src/services/admission/admission-router.ts +++ b/src/services/admission/admission-router.ts @@ -7,6 +7,8 @@ import Models from "../../database/models.js"; import { hasElevatedPerms } from "../auth/auth-lib.js"; import { ApplicantDecisionFormat } from "./admission-formats.js"; import { StatusCode } from "status-code-enum"; +import { NextFunction } from "express-serve-static-core"; +import { RouterError } from "../../middleware/error-handler.js"; const admissionRouter: Router = Router(); @@ -45,18 +47,21 @@ const admissionRouter: Router = Router(); * @apiError (500: Internal Server Error) {String} InternalError occurred on the server. * @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms. * */ -admissionRouter.get("/not-sent/", strongJwtVerification, async (_: Request, res: Response) => { +admissionRouter.get("/not-sent/", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { const token: JwtPayload = res.locals.payload as JwtPayload; if (!hasElevatedPerms(token)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } try { const filteredEntries: AdmissionDecision[] = await Models.AdmissionDecision.find({ emailSent: false }); return res.status(StatusCode.SuccessOK).send(filteredEntries); } catch (error) { - console.error(error); + if (error instanceof Error) { + return next(new RouterError(undefined, undefined, undefined, error.message)); + } else { + return next(new RouterError(undefined, undefined, undefined, `${error}`)); + } } - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InternalError" }); }); /** @@ -91,10 +96,10 @@ admissionRouter.get("/not-sent/", strongJwtVerification, async (_: Request, res: * @apiError (500: Internal Server Error) {String} InternalError occurred on the server. * @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms. * */ -admissionRouter.put("/", strongJwtVerification, async (req: Request, res: Response) => { +admissionRouter.put("/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const token: JwtPayload = res.locals.payload as JwtPayload; if (!hasElevatedPerms(token)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } const updateEntries: ApplicantDecisionFormat[] = req.body as ApplicantDecisionFormat[]; const ops = updateEntries.map((entry) => { @@ -104,9 +109,12 @@ admissionRouter.put("/", strongJwtVerification, async (req: Request, res: Respon await Promise.all(ops); return res.status(StatusCode.SuccessOK).send({ message: "StatusSuccess" }); } catch (error) { - console.log(error); + if (error instanceof Error) { + return next(new RouterError(undefined, undefined, undefined, error.message)); + } else { + return next(new RouterError(undefined, undefined, undefined, `${error}`)); + } } - return res.status(StatusCode.ClientErrorBadRequest).send("InternalError"); }); /** @@ -133,21 +141,21 @@ admissionRouter.put("/", strongJwtVerification, async (req: Request, res: Respon * * @apiUse strongVerifyErrors */ -admissionRouter.get("/rsvp/:USERID", strongJwtVerification, async (req: Request, res: Response) => { +admissionRouter.get("/rsvp/:USERID", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const userId: string | undefined = req.params.USERID; const payload: JwtPayload = res.locals.payload as JwtPayload; //Sends error if caller doesn't have elevated perms if (!hasElevatedPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } const queryResult: AdmissionDecision | null = await Models.AdmissionDecision.findOne({ userId: userId }); //Returns error if query is empty if (!queryResult) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "UserNotFound")); } return res.status(StatusCode.SuccessOK).send(queryResult); @@ -182,7 +190,7 @@ admissionRouter.get("/rsvp/:USERID", strongJwtVerification, async (req: Request, * * @apiUse strongVerifyErrors */ -admissionRouter.get("/rsvp", strongJwtVerification, async (_: Request, res: Response) => { +admissionRouter.get("/rsvp", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; const userId: string = payload.id; @@ -191,7 +199,7 @@ admissionRouter.get("/rsvp", strongJwtVerification, async (_: Request, res: Resp //Returns error if query is empty if (!queryResult) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "UserNotFound")); } //Filters data if caller doesn't have elevated perms @@ -232,12 +240,12 @@ admissionRouter.get("/rsvp", strongJwtVerification, async (_: Request, res: Resp * * @apiUse strongVerifyErrors */ -admissionRouter.put("/rsvp/", strongJwtVerification, async (req: Request, res: Response) => { +admissionRouter.put("/rsvp/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const rsvp: boolean | undefined = req.body.isAttending; //Returns error if request body has no isAttending parameter if (rsvp === undefined) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams")); } const payload: JwtPayload = res.locals.payload as JwtPayload; @@ -248,12 +256,12 @@ admissionRouter.put("/rsvp/", strongJwtVerification, async (req: Request, res: R //Returns error if query is empty if (!queryResult) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "UserNotFound")); } //If the current user has not been accepted, send an error if (queryResult.status != DecisionStatus.ACCEPTED) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "NotAccepted" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "NotAccepted")); } //If current user has been accepted, update their RSVP decision to "ACCEPTED"/"DECLINED" acoordingly @@ -267,10 +275,9 @@ admissionRouter.put("/rsvp/", strongJwtVerification, async (req: Request, res: R ); if (updatedDecision) { - //return res.status(StatusCode.SuccessOK).send(updatedDecision.toObject()); return res.status(StatusCode.SuccessOK).send(updatedDecision); } else { - return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" }); + return next(new RouterError()); } }); diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 24cd7269..d170f2e6 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -21,6 +21,7 @@ import { getUsersWithRole, } from "./auth-lib.js"; import Models from "../../database/models.js"; +import { RouterError } from "../../middleware/error-handler.js"; passport.use( Provider.GITHUB, @@ -53,13 +54,13 @@ authRouter.get("/test/", (_: Request, res: Response) => { res.end("Auth endpoint is working!"); }); -authRouter.get("/dev/", (req: Request, res: Response) => { +authRouter.get("/dev/", (req: Request, res: Response, next: NextFunction) => { const token: string | undefined = req.query.token as string | undefined; if (!token) { - res.status(StatusCode.ClientErrorBadRequest).send({ error: "NoToken" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "NoToken")); } - res.status(StatusCode.SuccessOK).send({ token: token }); + return res.status(StatusCode.SuccessOK).send({ token: token }); }); /** @@ -85,7 +86,7 @@ authRouter.get("/login/github/", (req: Request, res: Response, next: NextFunctio const device: string = (req.query.device as string | undefined) ?? Config.DEFAULT_DEVICE; if (device && !Config.REDIRECT_URLS.has(device)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "BadDevice" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "BadDevice")); } return SelectAuthProvider("github", device)(req, res, next); }); @@ -113,7 +114,7 @@ authRouter.get("/login/google/", (req: Request, res: Response, next: NextFunctio const device: string = (req.query.device as string | undefined) ?? Config.DEFAULT_DEVICE; if (device && !Config.REDIRECT_URLS.has(device)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "BadDevice" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "BadDevice")); } return SelectAuthProvider("google", device)(req, res, next); }); @@ -136,9 +137,9 @@ authRouter.get( console.error(error); } }, - async (req: Request, res: Response) => { + async (req: Request, res: Response, next: NextFunction) => { if (!req.isAuthenticated()) { - return res.status(StatusCode.ClientErrorUnauthorized).send({ error: "FailedAuth" }); + return next(new RouterError(StatusCode.ClientErrorUnauthorized, "FailedAuth")); } const device: string = (res.locals.device ?? Config.DEFAULT_DEVICE) as string; @@ -166,8 +167,7 @@ authRouter.get( const url: string = `${redirect}?token=${token}`; return res.redirect(url); } catch (error) { - console.error(error); - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidData" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidData")); } }, ); @@ -192,7 +192,7 @@ authRouter.get( * @apiError (400: Bad Request) {String} UserNotFound User doesn't exist in the database. * @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms. */ -authRouter.get("/roles/:USERID", strongJwtVerification, async (req: Request, res: Response) => { +authRouter.get("/roles/:USERID", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const targetUser: string | undefined = req.params.USERID; // Check if we have a user to get roles for - if not, get roles for current user @@ -210,11 +210,10 @@ authRouter.get("/roles/:USERID", strongJwtVerification, async (req: Request, res const roles: Role[] = await getRoles(targetUser); return res.status(StatusCode.SuccessOK).send({ id: targetUser, roles: roles }); } catch (error) { - console.error(error); - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "UserNotFound")); } } else { - return res.status(StatusCode.ClientErrorForbidden).send("Forbidden"); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } }); @@ -239,12 +238,12 @@ authRouter.get("/roles/:USERID", strongJwtVerification, async (req: Request, res * @apiError (400: Bad Request) {String} InvalidRole Nonexistent role passed in. * @apiUse strongVerifyErrors */ -authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, res: Response) => { +authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; // Not authenticated with modify roles perms if (!hasElevatedPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } // Parse to get operation type @@ -252,14 +251,14 @@ authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, // No operation - fail out if (!op) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidOperation" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidOperation")); } // Check if role to add/remove actually exists const data: ModifyRoleRequest = req.body as ModifyRoleRequest; const role: Role | undefined = Role[data.role.toUpperCase() as keyof typeof Role]; if (!role) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidRole" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidRole")); } // Try to update roles, if possible @@ -267,8 +266,7 @@ authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, const newRoles: Role[] = await updateRoles(data.id, role, op); return res.status(StatusCode.SuccessOK).send({ id: data.id, roles: newRoles }); } catch (error) { - console.error(error); - return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" }); + return next(new RouterError()); } }); @@ -289,12 +287,12 @@ authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, * @apiError (400: Bad Request) {String} UserNotFound User doesn't exist in the database * @apiError (403: Forbidden) {String} Forbidden API accessed by user without valid perms */ -authRouter.get("/list/roles/", strongJwtVerification, (_: Request, res: Response) => { +authRouter.get("/list/roles/", strongJwtVerification, (_: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; // Check if current user should be able to access all roles if (!hasElevatedPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } // Filter enum to get all possible string keys @@ -321,7 +319,7 @@ authRouter.get("/list/roles/", strongJwtVerification, (_: Request, res: Response * * @apiUse strongVerifyErrors */ -authRouter.get("/roles/", strongJwtVerification, async (_: Request, res: Response) => { +authRouter.get("/roles/", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; const targetUser: string = payload.id; @@ -330,8 +328,7 @@ authRouter.get("/roles/", strongJwtVerification, async (_: Request, res: Respons return res.status(StatusCode.SuccessOK).send({ id: targetUser, roles: roles }); }) .catch((error: Error) => { - console.error(error); - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "UserNotFound", undefined, error.message)); }); }); @@ -351,7 +348,7 @@ authRouter.get("/roles/", strongJwtVerification, async (_: Request, res: Respons * * @apiUse strongVerifyErrors */ -authRouter.get("/roles/list/:ROLE", async (req: Request, res: Response) => { +authRouter.get("/roles/list/:ROLE", async (req: Request, res: Response, next: NextFunction) => { const role: string | undefined = req.params.ROLE; //Returns error if role parameter is empty @@ -365,7 +362,7 @@ authRouter.get("/roles/list/:ROLE", async (req: Request, res: Response) => { }) .catch((error: Error) => { console.error(error); - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "Unknown Error" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "Unknown Error")); }); }); @@ -383,7 +380,7 @@ authRouter.get("/roles/list/:ROLE", async (req: Request, res: Response) => { * * @apiUse strongVerifyErrors */ -authRouter.get("/token/refresh", strongJwtVerification, async (_: Request, res: Response) => { +authRouter.get("/token/refresh", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { // Get old data from token const oldPayload: JwtPayload = res.locals.payload as JwtPayload; const data: ProfileData = { @@ -399,8 +396,7 @@ authRouter.get("/token/refresh", strongJwtVerification, async (_: Request, res: const newToken: string = generateJwtToken(newPayload); return res.status(StatusCode.SuccessOK).send({ token: newToken }); } catch (error) { - console.error(error); - return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" }); + return next(new RouterError()); } }); diff --git a/src/services/event/event-router.ts b/src/services/event/event-router.ts index 0700d955..55ba4016 100644 --- a/src/services/event/event-router.ts +++ b/src/services/event/event-router.ts @@ -24,6 +24,8 @@ import Models from "../../database/models.js"; import { ObjectId } from "mongodb"; import { StatusCode } from "status-code-enum"; +import { RouterError } from "../../middleware/error-handler.js"; + const eventsRouter: Router = Router(); eventsRouter.use(cors({ origin: "*" })); @@ -62,11 +64,11 @@ eventsRouter.use(cors({ origin: "*" })); * @apiUse strongVerifyErrors * @apiError (403: Forbidden) {String} Forbidden Not a valid staff token. * */ -eventsRouter.get("/staff/", strongJwtVerification, async (_: Request, res: Response) => { +eventsRouter.get("/staff/", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; if (!hasStaffPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } const staffEvents: StaffEvent[] = await Models.StaffEvent.find(); @@ -146,12 +148,12 @@ eventsRouter.get("/:EVENTID/", weakJwtVerification, async (req: Request, res: Re if (!metadata) { console.error("no metadata found!"); - return next(new Error("no event found!")); + return next(new RouterError(StatusCode.ClientErrorNotFound, "no event found!")); } if (metadata.isStaff) { if (!isStaff) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "PrivateEvent" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "PrivateEvent")); } const event: StaffEvent | null = await Models.StaffEvent.findOne({ eventId: eventId }); @@ -162,7 +164,7 @@ eventsRouter.get("/:EVENTID/", weakJwtVerification, async (req: Request, res: Re if (!event) { console.error("no metadata found!"); - return next(new Error("no event found!")); + return next(new RouterError(StatusCode.ClientErrorNotFound, "no event found!")); } const filteredEvent: FilteredEventView = createFilteredEventView(event); @@ -350,19 +352,19 @@ eventsRouter.get("/", weakJwtVerification, async (_: Request, res: Response) => * @apiError (403: Forbidden) {String} InvalidPermission User does not have admin permissions. * @apiError (500: Internal Server Error) {String} InternalError An error occurred on the server. */ -eventsRouter.post("/", strongJwtVerification, async (req: Request, res: Response) => { +eventsRouter.post("/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; // Check if the token has staff permissions if (!hasAdminPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "InvalidPermission")); } // Convert event format into the base event format const eventFormat: GenericEventFormat = req.body as GenericEventFormat; if (eventFormat.eventId) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "ExtraIdProvided" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "ExtraIdProvided", { extraEventId: eventFormat.eventId })); } // Create the ID and process metadata for this event @@ -380,14 +382,14 @@ eventsRouter.post("/", strongJwtVerification, async (req: Request, res: Response if (isStaffEvent) { // If ID doesn't exist -> return the invalid parameters if (!isValidStaffFormat(eventFormat)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams", eventFormat)); } const event: StaffEvent = new StaffEvent(eventFormat); console.log(event, metadata); newEvent = await Models.StaffEvent.create(event); } else { if (!isValidPublicFormat(eventFormat)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams", eventFormat)); } const event: PublicEvent = new PublicEvent(eventFormat); console.log(event, metadata); @@ -413,17 +415,17 @@ eventsRouter.post("/", strongJwtVerification, async (req: Request, res: Response * @apiError (403: Forbidden) {String} InvalidPermission User does not have admin permissions. * @apiError (500: Internal Server Error) {String} InternalError An error occurred on the server while deleting the event. */ -eventsRouter.delete("/:EVENTID/", strongJwtVerification, async (req: Request, res: Response) => { +eventsRouter.delete("/:EVENTID/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const eventId: string | undefined = req.params.EVENTID; // Check if request sender has permission to delete the event if (!hasAdminPerms(res.locals.payload as JwtPayload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "InvalidPermission")); } // Check if eventid field doesn't exist -> if not, returns error if (!eventId) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams")); } // Perform a lazy delete on both databases, and return true if the operation succeeds @@ -457,18 +459,18 @@ eventsRouter.delete("/:EVENTID/", strongJwtVerification, async (req: Request, re * @apiError (403: Forbidden) {String} InvalidPermission User does not have staff permissions. * @apiError (500: Internal Server Error) {String} InternalError An error occurred on the server while fetching metadata. */ -eventsRouter.get("/metadata/:EVENTID", strongJwtVerification, async (req: Request, res: Response) => { +eventsRouter.get("/metadata/:EVENTID", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; if (!hasStaffPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "InvalidPermission")); } // Check if the request information is valid const eventId: string | undefined = req.params.EVENTID; const metadata: EventMetadata | null = await Models.EventMetadata.findOne({ eventId: eventId }); if (!metadata) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "EventNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "EventNotFound")); } return res.status(StatusCode.SuccessOK).send(metadata); }); @@ -498,17 +500,17 @@ eventsRouter.get("/metadata/:EVENTID", strongJwtVerification, async (req: Reques * @apiError (403: Forbidden) {String} InvalidPermission User does not have admin permissions. * @apiError (500: Internal Server Error) {String} InternalError An error occurred on the server while updating metadata. */ -eventsRouter.put("/metadata/", strongJwtVerification, async (req: Request, res: Response) => { +eventsRouter.put("/metadata/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; if (!hasAdminPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "InvalidPermission")); } // Check if the request information is valid const metadata: MetadataFormat = req.body as MetadataFormat; if (!isValidMetadataFormat(metadata)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams", metadata)); } // Update the database, and return true if it passes. Else, return false. @@ -518,7 +520,7 @@ eventsRouter.put("/metadata/", strongJwtVerification, async (req: Request, res: ); if (!metadata) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "EventNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "EventNotFound")); } return res.status(StatusCode.SuccessOK).send(updatedMetadata); @@ -591,12 +593,12 @@ eventsRouter.put("/metadata/", strongJwtVerification, async (req: Request, res: * @apiError (400: Bad Request) {String} Bad Request Invalid parameters or event format. * @apiError (500: Internal Server Error) {String} InternalError An internal error occurred. */ -eventsRouter.put("/", strongJwtVerification, async (req: Request, res: Response) => { +eventsRouter.put("/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; // Check if the token has elevated permissions if (!hasAdminPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "InvalidPermission")); } // Verify that the input format is valid to create a new event @@ -605,18 +607,18 @@ eventsRouter.put("/", strongJwtVerification, async (req: Request, res: Response) console.log(eventFormat.eventId); if (!eventId) { - return res.status(StatusCode.ClientErrorBadRequest).send({ message: "NoEventId" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "NoEventId")); } const metadata: EventMetadata | null = await Models.EventMetadata.findOne({ eventId: eventFormat.eventId }); if (!metadata) { - return res.status(StatusCode.ClientErrorBadRequest).send({ message: "EventNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "EventNotFound", metadata)); } if (metadata.isStaff) { if (!isValidStaffFormat(eventFormat)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ message: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams", eventFormat)); } const event: StaffEvent = new StaffEvent(eventFormat); @@ -624,7 +626,7 @@ eventsRouter.put("/", strongJwtVerification, async (req: Request, res: Response) return res.status(StatusCode.SuccessOK).send(updatedEvent); } else { if (!isValidPublicFormat(eventFormat)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ message: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams", eventFormat)); } const event: PublicEvent = new PublicEvent(eventFormat); diff --git a/src/services/newsletter/newsletter-router.ts b/src/services/newsletter/newsletter-router.ts index 5bf8cfb6..3c50a887 100644 --- a/src/services/newsletter/newsletter-router.ts +++ b/src/services/newsletter/newsletter-router.ts @@ -8,6 +8,8 @@ import Models from "../../database/models.js"; import { UpdateQuery } from "mongoose"; import { StatusCode } from "status-code-enum"; import Config from "../../config.js"; +import { RouterError } from "../../middleware/error-handler.js"; +import { NextFunction } from "express-serve-static-core"; const newsletterRouter: Router = Router(); @@ -50,14 +52,14 @@ newsletterRouter.use(cors(corsOptions)); * HTTP/1.1 400 Bad Request * {"error": "InvalidParams"} */ -newsletterRouter.post("/subscribe/", async (request: Request, res: Response) => { +newsletterRouter.post("/subscribe/", async (request: Request, res: Response, next: NextFunction) => { const requestBody: SubscribeRequest = request.body as SubscribeRequest; const listName: string | undefined = requestBody.listName; const emailAddress: string | undefined = requestBody.emailAddress; // Verify that both parameters do exist if (!listName || !emailAddress) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams")); } // Perform a lazy delete diff --git a/src/services/profile/profile-router.ts b/src/services/profile/profile-router.ts index deab573c..22852005 100644 --- a/src/services/profile/profile-router.ts +++ b/src/services/profile/profile-router.ts @@ -1,6 +1,6 @@ import cors from "cors"; import { Request, Router } from "express"; -import { Response } from "express-serve-static-core"; +import { NextFunction, Response } from "express-serve-static-core"; import Config from "../../config.js"; import { isValidLimit } from "./profile-lib.js"; @@ -16,6 +16,8 @@ import { hasElevatedPerms } from "../auth/auth-lib.js"; import { DeleteResult } from "mongodb"; import { StatusCode } from "status-code-enum"; +import { RouterError } from "../../middleware/error-handler.js"; + const profileRouter: Router = Router(); profileRouter.use(cors({ origin: "*" })); @@ -50,7 +52,7 @@ profileRouter.use(cors({ origin: "*" })); * HTTP/1.1 400 Bad Request * {"error": "InvalidInput"} */ -profileRouter.get("/leaderboard/", async (req: Request, res: Response) => { +profileRouter.get("/leaderboard/", async (req: Request, res: Response, next: NextFunction) => { const limitString: string | undefined = req.query.limit as string | undefined; // Initialize the metadata @@ -62,7 +64,7 @@ profileRouter.get("/leaderboard/", async (req: Request, res: Response) => { // Check for limit validity if (!limit || !isValidLimit) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidLimit" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidLimit")); } // if the limit is above the leaderboard query limit, set it to the query limit @@ -117,7 +119,7 @@ profileRouter.get("/leaderboard/", async (req: Request, res: Response) => { * {"error": "InternalError"} */ -profileRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => { +profileRouter.get("/", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { const payload: JwtPayload = res.locals.payload as JwtPayload; const userId: string = payload.id; @@ -125,7 +127,7 @@ profileRouter.get("/", strongJwtVerification, async (_: Request, res: Response) const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: userId }); if (!user) { - return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "UserNotFound")); } return res.status(StatusCode.SuccessOK).send(user); @@ -166,19 +168,19 @@ profileRouter.get("/", strongJwtVerification, async (_: Request, res: Response) * {"error": "InternalError"} */ -profileRouter.get("/id/:USERID", strongJwtVerification, async (req: Request, res: Response) => { +profileRouter.get("/id/:USERID", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const userId: string | undefined = req.params.USERID; const payload: JwtPayload = res.locals.payload as JwtPayload; // Trying to perform elevated operation (getting someone else's profile without elevated perms) if (!hasElevatedPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: userId }); if (!user) { - return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "UserNotFound")); } return res.status(StatusCode.SuccessOK).send(user); @@ -226,7 +228,7 @@ profileRouter.get("/id", (_: Request, res: Response) => { * HTTP/1.1 500 Internal Server Error * {"error": "InternalError"} */ -profileRouter.post("/", strongJwtVerification, async (req: Request, res: Response) => { +profileRouter.post("/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const profile: ProfileFormat = req.body as ProfileFormat; profile.points = Config.DEFAULT_POINT_VALUE; @@ -234,13 +236,13 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons profile.userId = payload.id; if (!isValidProfileFormat(profile)) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams")); } // Ensure that user doesn't already exist before creating const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: profile.userId }); if (user) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserAlreadyExists" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "UserAlreadyExists")); } // Create a metadata object, and return it @@ -251,7 +253,7 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons return res.status(StatusCode.SuccessOK).send(newProfile); } catch (error) { console.error(error); - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams")); } }); @@ -273,14 +275,14 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons * {"error": "InternalError"} */ -profileRouter.delete("/", strongJwtVerification, async (_: Request, res: Response) => { +profileRouter.delete("/", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { const decodedData: JwtPayload = res.locals.payload as JwtPayload; const attendeeProfileDeleteResponse: DeleteResult = await Models.AttendeeProfile.deleteOne({ userId: decodedData.id }); const attendeeMetadataDeleteResponse: DeleteResult = await Models.AttendeeMetadata.deleteOne({ userId: decodedData.id }); if (attendeeMetadataDeleteResponse.deletedCount == 0 || attendeeProfileDeleteResponse.deletedCount == 0) { - return res.status(StatusCode.ClientErrorNotFound).send({ success: false, error: "AttendeeNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "AttendeeNotFound")); } return res.status(StatusCode.SuccessOK).send({ success: true }); }); diff --git a/src/services/staff/staff-router.ts b/src/services/staff/staff-router.ts index 927fae5d..00706849 100644 --- a/src/services/staff/staff-router.ts +++ b/src/services/staff/staff-router.ts @@ -10,6 +10,8 @@ import Config from "../../config.js"; import { EventMetadata } from "../../database/event-db.js"; import Models from "../../database/models.js"; import { StatusCode } from "status-code-enum"; +import { NextFunction } from "express-serve-static-core"; +import { RouterError } from "../../middleware/error-handler.js"; const staffRouter: Router = Router(); @@ -41,31 +43,31 @@ const staffRouter: Router = Router(); * HTTP/1.1 500 Internal Server Error * {"error": "InternalError"} */ -staffRouter.post("/attendance/", strongJwtVerification, async (req: Request, res: Response) => { +staffRouter.post("/attendance/", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const payload: JwtPayload | undefined = res.locals.payload as JwtPayload; const eventId: string | undefined = (req.body as AttendanceFormat).eventId; const userId: string = payload.id; // Only staff can mark themselves as attending these events if (!hasStaffPerms(payload)) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } if (!eventId) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "InvalidParams")); } const metadata: EventMetadata | null = await Models.EventMetadata.findOne({ eventId: eventId }); if (!metadata) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "EventNotFound" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "EventNotFound")); } const timestamp: number = Math.round(Date.now() / Config.MILLISECONDS_PER_SECOND); console.log(metadata.exp, timestamp); if (metadata.exp <= timestamp) { - return res.status(StatusCode.ClientErrorBadRequest).send({ error: "CodeExpired" }); + return next(new RouterError(StatusCode.ClientErrorBadRequest, "CodeExpired")); } await Models.UserAttendance.findOneAndUpdate({ userId: userId }, { $addToSet: { attendance: eventId } }, { upsert: true }); diff --git a/src/services/user/user-router.ts b/src/services/user/user-router.ts index 70482d20..efef2704 100644 --- a/src/services/user/user-router.ts +++ b/src/services/user/user-router.ts @@ -9,6 +9,8 @@ import { generateJwtToken, getJwtPayloadFromDB, hasElevatedPerms, hasStaffPerms import { UserInfo } from "../../database/user-db.js"; import Models from "../../database/models.js"; import Config from "../../config.js"; +import { NextFunction } from "express-serve-static-core"; +import { RouterError } from "../../middleware/error-handler.js"; const userRouter: Router = Router(); @@ -60,7 +62,7 @@ userRouter.get("/qr/", strongJwtVerification, (_: Request, res: Response) => { * @apiError (403: Forbidden) {String} Forbidden API access by user (no valid perms). * @apiUse strongVerifyErrors */ -userRouter.get("/qr/:USERID", strongJwtVerification, async (req: Request, res: Response) => { +userRouter.get("/qr/:USERID", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const targetUser: string | undefined = req.params.USERID as string; const payload: JwtPayload = res.locals.payload as JwtPayload; @@ -76,7 +78,7 @@ userRouter.get("/qr/:USERID", strongJwtVerification, async (req: Request, res: R // Return false if we haven't created a payload yet if (!newPayload) { - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); } // Generate the token @@ -107,7 +109,7 @@ userRouter.get("/qr/:USERID", strongJwtVerification, async (req: Request, res: R * @apiError (403: Forbidden) {String} Forbidden API access by user (no valid perms). * @apiUse strongVerifyErrors */ -userRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Response) => { +userRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Response, next: NextFunction) => { const targetUser: string = req.params.USERID ?? ""; // Get payload, and check if authorized @@ -118,11 +120,11 @@ userRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Resp if (userInfo) { return res.status(StatusCode.SuccessOK).send(userInfo); } else { - return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "UserNotFound")); } } - return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); + return next(new RouterError(StatusCode.ClientErrorForbidden, "Forbidden")); }); /** @@ -144,7 +146,7 @@ userRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Resp * * @apiUse strongVerifyErrors */ -userRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => { +userRouter.get("/", strongJwtVerification, async (_: Request, res: Response, next: NextFunction) => { // Get payload, return user's values const payload: JwtPayload = res.locals.payload as JwtPayload; @@ -153,7 +155,7 @@ userRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => if (user) { return res.status(StatusCode.SuccessOK).send(user); } else { - return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" }); + return next(new RouterError(StatusCode.ClientErrorNotFound, "UserNotFound")); } });