Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added Error Handling middleware #121

Merged
merged 11 commits into from
Nov 15, 2023
5 changes: 4 additions & 1 deletion src/app.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -52,6 +53,8 @@ export function setupServer(): void {
Models.initialize();
}

app.use(ErrorHandler);

export function startServer(): Promise<Express.Application> {
// eslint-disable-next-line no-magic-numbers
const port = Config.PORT;
Expand Down
48 changes: 48 additions & 0 deletions src/middleware/error-handler.ts
Original file line number Diff line number Diff line change
@@ -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): void {
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;
}

resp.status(statusCode).json(jsonData);
akulsharma1 marked this conversation as resolved.
Show resolved Hide resolved
}
45 changes: 26 additions & 19 deletions src/services/admission/admission-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();

Expand Down Expand Up @@ -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" });
});

/**
Expand Down Expand Up @@ -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) => {
Expand All @@ -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");
});

/**
Expand All @@ -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);
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand Down Expand Up @@ -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;
Expand All @@ -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
Expand All @@ -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());
}
});

Expand Down
Loading