diff --git a/.eslintrc.cjs b/.eslintrc.cjs index aae15c40..9a4dbc09 100644 --- a/.eslintrc.cjs +++ b/.eslintrc.cjs @@ -40,6 +40,7 @@ module.exports = { files: ["**/*.test.ts"], // Disable specific rules for tests rules: { "no-magic-numbers": "off", + "@typescript-eslint/no-var-requires": "off", // Required for jest }, }, ], diff --git a/package.json b/package.json index 17b09c2a..be94f54c 100644 --- a/package.json +++ b/package.json @@ -41,6 +41,7 @@ "passport-github": "^1.1.0", "passport-google-oauth20": "^2.0.0", "passport-strategy": "^1.0.0", + "status-code-enum": "^1.0.0", "uuid": "^9.0.1" }, "repository": "git@github.com:HackIllinois/adonix.git", diff --git a/src/app.test.ts b/src/app.test.ts index 2401769b..e0c2a41e 100644 --- a/src/app.test.ts +++ b/src/app.test.ts @@ -3,6 +3,7 @@ import { describe, expect, test, it } from "@jest/globals"; import { get } from "./testTools.js"; +import { StatusCode } from "status-code-enum"; describe("sanity tests for app", () => { test("life is not a lie", () => { @@ -10,7 +11,7 @@ describe("sanity tests for app", () => { }); it("should run", async () => { - const response = await get("/").expect(200); + const response = await get("/").expect(StatusCode.SuccessOK); expect(response.text).toBe("API is working!!!"); }); diff --git a/src/app.ts b/src/app.ts index b1ad8880..e5d76408 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,7 +3,6 @@ import { TEST } from "./env.js"; import morgan from "morgan"; import express, { Application, Request, Response } from "express"; -import Constants from "./constants.js"; import authRouter from "./services/auth/auth-router.js"; import userRouter from "./services/user/user-router.js"; import eventRouter from "./services/event/event-router.js"; @@ -14,6 +13,7 @@ import versionRouter from "./services/version/version-router.js"; import { InitializeConfigReader } from "./middleware/config-reader.js"; import Models from "./database/models.js"; +import { StatusCode } from "status-code-enum"; const app: Application = express(); @@ -46,7 +46,7 @@ app.get("/", (_: Request, res: Response) => { // Throw an error if call is made to the wrong API endpoint app.use("/", (_: Request, res: Response) => { - res.status(Constants.NOT_FOUND).end("API endpoint does not exist!"); + res.status(StatusCode.ClientErrorNotFound).end("API endpoint does not exist!"); }); export function setupServer(): void { diff --git a/src/constants.ts b/src/constants.ts index dbf9ce8e..a42595ce 100644 --- a/src/constants.ts +++ b/src/constants.ts @@ -1,16 +1,4 @@ abstract class Constants { - // Status codes - static readonly SUCCESS: number = 200; - static readonly CREATED: number = 201; - static readonly NO_CONTENT: number = 204; - static readonly BAD_REQUEST: number = 400; - static readonly FAILURE: number = 400; - static readonly UNAUTHORIZED_REQUEST: number = 401; - static readonly FORBIDDEN: number = 403; - static readonly NOT_FOUND: number = 404; - static readonly OLD_API: number = 418; - static readonly INTERNAL_ERROR: number = 500; - // URLs static readonly ADMIN_DEVICE: string = "admin"; static readonly DEV_DEVICE: string = "dev"; @@ -54,6 +42,7 @@ abstract class Constants { static readonly DEFAULT_POINT_VALUE: number = 0; static readonly DEFAULT_FOOD_WAVE: number = 0; static readonly LEADERBOARD_QUERY_LIMIT: number = 25; + static readonly QR_EXPIRY_TIME: string = "20s"; } export default Constants; diff --git a/src/middleware/verify-jwt.ts b/src/middleware/verify-jwt.ts index 2f213a86..e3f6c7d7 100644 --- a/src/middleware/verify-jwt.ts +++ b/src/middleware/verify-jwt.ts @@ -1,7 +1,7 @@ import { Request, Response, NextFunction } from "express"; -import Constants from "../constants.js"; import { decodeJwtToken } from "../services/auth/auth-lib.js"; import jsonwebtoken from "jsonwebtoken"; +import { StatusCode } from "status-code-enum"; /** * @apiDefine strongVerifyErrors @@ -22,7 +22,7 @@ export function strongJwtVerification(req: Request, res: Response, next: NextFun const token: string | undefined = req.headers.authorization; if (!token) { - res.status(Constants.UNAUTHORIZED_REQUEST).send({ error: "NoToken" }); + res.status(StatusCode.ClientErrorUnauthorized).send({ error: "NoToken" }); next("router"); return; } @@ -33,10 +33,10 @@ export function strongJwtVerification(req: Request, res: Response, next: NextFun } catch (error) { console.error(error); if (error instanceof jsonwebtoken.TokenExpiredError) { - res.status(Constants.FORBIDDEN).send("TokenExpired"); + res.status(StatusCode.ClientErrorForbidden).send("TokenExpired"); next("router"); } else { - res.status(Constants.UNAUTHORIZED_REQUEST).send({ + res.status(StatusCode.ClientErrorUnauthorized).send({ error: "InvalidToken", }); next("router"); diff --git a/src/services/auth/auth-router.ts b/src/services/auth/auth-router.ts index 4c424a5a..927852cb 100644 --- a/src/services/auth/auth-router.ts +++ b/src/services/auth/auth-router.ts @@ -5,6 +5,7 @@ import GitHubStrategy, { Profile as GithubProfile } from "passport-github"; import { Strategy as GoogleStrategy, Profile as GoogleProfile } from "passport-google-oauth20"; import Constants from "../../constants.js"; +import { StatusCode } from "status-code-enum"; import { strongJwtVerification } from "../../middleware/verify-jwt.js"; import { SelectAuthProvider } from "../../middleware/select-auth.js"; @@ -56,10 +57,10 @@ authRouter.get("/test/", (_: Request, res: Response) => { authRouter.get("/dev/", (req: Request, res: Response) => { const token: string | undefined = req.query.token as string | undefined; if (!token) { - res.status(Constants.BAD_REQUEST).send({ error: "NoToken" }); + res.status(StatusCode.ClientErrorBadRequest).send({ error: "NoToken" }); } - res.status(Constants.SUCCESS).send({ token: token }); + 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) ?? Constants.DEFAULT_DEVICE; if (device && !Constants.REDIRECT_MAPPINGS.has(device)) { - return res.status(Constants.BAD_REQUEST).send({ error: "BadDevice" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "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) ?? Constants.DEFAULT_DEVICE; if (device && !Constants.REDIRECT_MAPPINGS.has(device)) { - return res.status(Constants.BAD_REQUEST).send({ error: "BadDevice" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "BadDevice" }); } return SelectAuthProvider("google", device)(req, res, next); }); @@ -133,7 +134,7 @@ authRouter.get( }, async (req: Request, res: Response) => { if (!req.isAuthenticated()) { - return res.status(Constants.UNAUTHORIZED_REQUEST).send({ error: "FailedAuth" }); + return res.status(StatusCode.ClientErrorUnauthorized).send({ error: "FailedAuth" }); } const device: string = (res.locals.device ?? Constants.DEFAULT_DEVICE) as string; @@ -162,7 +163,7 @@ authRouter.get( return res.redirect(url); } catch (error) { console.error(error); - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidData" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidData" }); } }, ); @@ -199,17 +200,17 @@ authRouter.get("/roles/:USERID", strongJwtVerification, async (req: Request, res // Cases: Target user already logged in, auth user is admin if (payload.id == targetUser) { - return res.status(Constants.SUCCESS).send({ id: payload.id, roles: payload.roles }); + return res.status(StatusCode.SuccessOK).send({ id: payload.id, roles: payload.roles }); } else if (hasElevatedPerms(payload)) { try { const roles: Role[] = await getRoles(targetUser); - return res.status(Constants.SUCCESS).send({ id: targetUser, roles: roles }); + return res.status(StatusCode.SuccessOK).send({ id: targetUser, roles: roles }); } catch (error) { console.error(error); - return res.status(Constants.BAD_REQUEST).send({ error: "UserNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); } } else { - return res.status(Constants.FORBIDDEN).send("Forbidden"); + return res.status(StatusCode.ClientErrorForbidden).send("Forbidden"); } }); @@ -239,7 +240,7 @@ authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, // Not authenticated with modify roles perms if (!hasElevatedPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "Forbidden" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } // Parse to get operation type @@ -247,23 +248,23 @@ authRouter.put("/roles/:OPERATION/", strongJwtVerification, async (req: Request, // No operation - fail out if (!op) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidOperation" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "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(Constants.BAD_REQUEST).send({ error: "InvalidRole" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidRole" }); } // Try to update roles, if possible try { const newRoles: Role[] = await updateRoles(data.id, role, op); - return res.status(Constants.SUCCESS).send({ id: data.id, roles: newRoles }); + return res.status(StatusCode.SuccessOK).send({ id: data.id, roles: newRoles }); } catch (error) { console.error(error); - return res.status(Constants.INTERNAL_ERROR).send({ error: "InternalError" }); + return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" }); } }); @@ -289,7 +290,7 @@ authRouter.get("/list/roles/", strongJwtVerification, (_: Request, res: Response // Check if current user should be able to access all roles if (!hasElevatedPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "Forbidden" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } // Filter enum to get all possible string keys @@ -297,7 +298,7 @@ authRouter.get("/list/roles/", strongJwtVerification, (_: Request, res: Response return isNaN(Number(item)); }); - return res.status(Constants.SUCCESS).send({ roles: roles }); + return res.status(StatusCode.SuccessOK).send({ roles: roles }); }); /** @@ -322,11 +323,11 @@ authRouter.get("/roles/", strongJwtVerification, async (_: Request, res: Respons await getRoles(targetUser) .then((roles: Role[]) => { - return res.status(Constants.SUCCESS).send({ id: targetUser, roles: roles }); + return res.status(StatusCode.SuccessOK).send({ id: targetUser, roles: roles }); }) .catch((error: Error) => { console.error(error); - return res.status(Constants.BAD_REQUEST).send({ error: "UserNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); }); }); @@ -351,16 +352,16 @@ authRouter.get("/roles/list/:ROLE", async (req: Request, res: Response) => { //Returns error if role parameter is empty if (!role) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } return await getUsersWithRole(role) .then((users: string[]) => { - return res.status(Constants.SUCCESS).send({ userIds: users }); + return res.status(StatusCode.SuccessOK).send({ userIds: users }); }) .catch((error: Error) => { console.error(error); - return res.status(Constants.BAD_REQUEST).send({ error: "Unknown Error" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "Unknown Error" }); }); }); @@ -392,10 +393,10 @@ authRouter.get("/token/refresh", strongJwtVerification, async (_: Request, res: // Create and return a new token with the payload const newToken: string = generateJwtToken(newPayload); - return res.status(Constants.SUCCESS).send({ token: newToken }); + return res.status(StatusCode.SuccessOK).send({ token: newToken }); } catch (error) { console.error(error); - return res.status(Constants.INTERNAL_ERROR).send({ error: "InternalError" }); + return res.status(StatusCode.ServerErrorInternal).send({ error: "InternalError" }); } }); diff --git a/src/services/event/event-router.ts b/src/services/event/event-router.ts index c8523803..6c25d35d 100644 --- a/src/services/event/event-router.ts +++ b/src/services/event/event-router.ts @@ -22,6 +22,7 @@ import { FilteredEventView } from "./event-models.js"; import { EventMetadata, PublicEvent, StaffEvent } from "../../database/event-db.js"; import Models from "../../database/models.js"; import { ObjectId } from "mongodb"; +import { StatusCode } from "status-code-enum"; const eventsRouter: Router = Router(); eventsRouter.use(cors({ origin: "*" })); @@ -65,11 +66,11 @@ eventsRouter.get("/staff/", strongJwtVerification, async (_: Request, res: Respo const payload: JwtPayload = res.locals.payload as JwtPayload; if (!hasStaffPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "Forbidden" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } const staffEvents: StaffEvent[] = await Models.StaffEvent.find(); - return res.status(Constants.SUCCESS).send({ events: staffEvents }); + return res.status(StatusCode.SuccessOK).send({ events: staffEvents }); }); /** @@ -150,11 +151,11 @@ eventsRouter.get("/:EVENTID/", weakJwtVerification, async (req: Request, res: Re if (metadata.isStaff) { if (!isStaff) { - return res.status(Constants.FORBIDDEN).send({ error: "PrivateEvent" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "PrivateEvent" }); } const event: StaffEvent | null = await Models.StaffEvent.findOne({ eventId: eventId }); - return res.status(Constants.SUCCESS).send({ event: event }); + return res.status(StatusCode.SuccessOK).send({ event: event }); } else { // Not a private event -> convert to Public event and return const event: PublicEvent | null = await Models.PublicEvent.findOne({ eventId: eventId }); @@ -165,7 +166,7 @@ eventsRouter.get("/:EVENTID/", weakJwtVerification, async (req: Request, res: Re } const filteredEvent: FilteredEventView = createFilteredEventView(event); - return res.status(Constants.SUCCESS).send({ event: filteredEvent }); + return res.status(StatusCode.SuccessOK).send({ event: filteredEvent }); } }); @@ -237,10 +238,10 @@ eventsRouter.get("/", weakJwtVerification, async (_: Request, res: Response) => const publicEvents: PublicEvent[] = await Models.PublicEvent.find(); if (hasStaffPerms(payload)) { - return res.status(Constants.SUCCESS).send({ events: publicEvents }); + return res.status(StatusCode.SuccessOK).send({ events: publicEvents }); } else { const filteredEvents: FilteredEventView[] = publicEvents.map(createFilteredEventView); - return res.status(Constants.SUCCESS).send({ events: filteredEvents }); + return res.status(StatusCode.SuccessOK).send({ events: filteredEvents }); } }); @@ -350,14 +351,14 @@ eventsRouter.post("/", strongJwtVerification, async (req: Request, res: Response // Check if the token has staff permissions if (!hasAdminPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "InvalidPermission" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); } // Convert event format into the base event format const eventFormat: GenericEventFormat = req.body as GenericEventFormat; if (eventFormat.eventId) { - return res.status(Constants.BAD_REQUEST).send({ error: "ExtraIdProvided" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "ExtraIdProvided" }); } // Create the ID and process metadata for this event @@ -375,21 +376,21 @@ 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(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } const event: StaffEvent = new StaffEvent(eventFormat); console.log(event, metadata); newEvent = await Models.StaffEvent.create(event); } else { if (!isValidPublicFormat(eventFormat)) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } const event: PublicEvent = new PublicEvent(eventFormat); console.log(event, metadata); newEvent = await Models.PublicEvent.create(event); } await Models.EventMetadata.create(metadata); - return res.status(Constants.CREATED).send(newEvent); + return res.status(StatusCode.SuccessCreated).send(newEvent); }); /** @@ -413,12 +414,12 @@ eventsRouter.delete("/:EVENTID/", strongJwtVerification, async (req: Request, re // Check if request sender has permission to delete the event if (!hasAdminPerms(res.locals.payload as JwtPayload)) { - return res.status(Constants.FORBIDDEN).send({ error: "InvalidPermission" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); } // Check if eventid field doesn't exist -> if not, returns error if (!eventId) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } // Perform a lazy delete on both databases, and return true if the operation succeeds @@ -426,7 +427,7 @@ eventsRouter.delete("/:EVENTID/", strongJwtVerification, async (req: Request, re await Models.PublicEvent.findOneAndDelete({ eventId: eventId }); await Models.EventMetadata.findOneAndDelete({ eventId: eventId }); - return res.status(Constants.NO_CONTENT).send({ status: "Success" }); + return res.status(StatusCode.SuccessNoContent).send({ status: "Success" }); }); /** @@ -456,16 +457,16 @@ eventsRouter.get("/metadata/:EVENTID", strongJwtVerification, async (req: Reques const payload: JwtPayload = res.locals.payload as JwtPayload; if (!hasStaffPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "InvalidPermission" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "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(Constants.BAD_REQUEST).send({ error: "EventNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "EventNotFound" }); } - return res.status(Constants.SUCCESS).send(metadata); + return res.status(StatusCode.SuccessOK).send(metadata); }); /** @@ -497,13 +498,13 @@ eventsRouter.put("/metadata/", strongJwtVerification, async (req: Request, res: const payload: JwtPayload = res.locals.payload as JwtPayload; if (!hasAdminPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "InvalidPermission" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); } // Check if the request information is valid const metadata: MetadataFormat = req.body as MetadataFormat; if (!isValidMetadataFormat(metadata)) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } // Update the database, and return true if it passes. Else, return false. @@ -513,10 +514,10 @@ eventsRouter.put("/metadata/", strongJwtVerification, async (req: Request, res: ); if (!metadata) { - return res.status(Constants.BAD_REQUEST).send({ error: "EventNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "EventNotFound" }); } - return res.status(Constants.SUCCESS).send(updatedMetadata); + return res.status(StatusCode.SuccessOK).send(updatedMetadata); }); /** @@ -591,7 +592,7 @@ eventsRouter.put("/", strongJwtVerification, async (req: Request, res: Response) // Check if the token has elevated permissions if (!hasAdminPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "InvalidPermission" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "InvalidPermission" }); } // Verify that the input format is valid to create a new event @@ -600,42 +601,42 @@ eventsRouter.put("/", strongJwtVerification, async (req: Request, res: Response) console.log(eventFormat.eventId); if (!eventId) { - return res.status(Constants.BAD_REQUEST).send({ message: "NoEventId" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ message: "NoEventId" }); } const metadata: EventMetadata | null = await Models.EventMetadata.findOne({ eventId: eventFormat.eventId }); if (!metadata) { - return res.status(Constants.BAD_REQUEST).send({ message: "EventNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ message: "EventNotFound" }); } if (metadata.isStaff) { if (!isValidStaffFormat(eventFormat)) { - return res.status(Constants.BAD_REQUEST).send({ message: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ message: "InvalidParams" }); } const event: StaffEvent = new StaffEvent(eventFormat); const updatedEvent: StaffEvent | null = await Models.StaffEvent.findOneAndUpdate({ eventId: eventId }, event); - return res.status(Constants.SUCCESS).send(updatedEvent); + return res.status(StatusCode.SuccessOK).send(updatedEvent); } else { if (!isValidPublicFormat(eventFormat)) { - return res.status(Constants.BAD_REQUEST).send({ message: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ message: "InvalidParams" }); } const event: PublicEvent = new PublicEvent(eventFormat); const updatedEvent: PublicEvent | null = await Models.PublicEvent.findOneAndUpdate({ eventId: eventId }, event); - return res.status(Constants.SUCCESS).send(updatedEvent); + return res.status(StatusCode.SuccessOK).send(updatedEvent); } }); // Prototype error handler eventsRouter.use((err: Error, req: Request, res: Response) => { if (!err) { - return res.status(Constants.SUCCESS).send({ status: "OK" }); + return res.status(StatusCode.SuccessOK).send({ status: "OK" }); } console.error(err.stack, req.body); - return res.status(Constants.INTERNAL_ERROR).send({ error: err.message }); + return res.status(StatusCode.ServerErrorInternal).send({ error: err.message }); }); export default eventsRouter; diff --git a/src/services/newsletter/newsletter-router.ts b/src/services/newsletter/newsletter-router.ts index f5ec86b6..7be9939e 100644 --- a/src/services/newsletter/newsletter-router.ts +++ b/src/services/newsletter/newsletter-router.ts @@ -2,12 +2,11 @@ import { Request, Response, Router } from "express"; import { regexPasses } from "./newsletter-lib.js"; import cors, { CorsOptions } from "cors"; -import Constants from "../../constants.js"; - import { SubscribeRequest } from "./newsletter-formats.js"; import { NewsletterSubscription } from "../../database/newsletter-db.js"; import Models from "../../database/models.js"; import { UpdateQuery } from "mongoose"; +import { StatusCode } from "status-code-enum"; const newsletterRouter: Router = Router(); @@ -57,13 +56,13 @@ newsletterRouter.post("/subscribe/", async (request: Request, res: Response) => // Verify that both parameters do exist if (!listName || !emailAddress) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } // Perform a lazy delete const updateQuery: UpdateQuery = { $addToSet: { subscribers: emailAddress } }; await Models.NewsletterSubscription.findOneAndUpdate({ newsletterId: listName }, updateQuery, { upsert: true }); - return res.status(Constants.SUCCESS).send({ status: "Success" }); + return res.status(StatusCode.SuccessOK).send({ status: "Success" }); }); export default newsletterRouter; diff --git a/src/services/profile/profile-router.ts b/src/services/profile/profile-router.ts index 02b9f181..34b8a8a2 100644 --- a/src/services/profile/profile-router.ts +++ b/src/services/profile/profile-router.ts @@ -14,6 +14,7 @@ import { strongJwtVerification } from "../../middleware/verify-jwt.js"; import { ProfileFormat, isValidProfileFormat } from "./profile-formats.js"; import { hasElevatedPerms } from "../auth/auth-lib.js"; import { DeleteResult } from "mongodb"; +import { StatusCode } from "status-code-enum"; const profileRouter: Router = Router(); @@ -61,7 +62,7 @@ profileRouter.get("/leaderboard/", async (req: Request, res: Response) => { // Check for limit validity if (!limit || !isValidLimit) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidLimit" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidLimit" }); } // if the limit is above the leaderboard query limit, set it to the query limit @@ -79,7 +80,7 @@ profileRouter.get("/leaderboard/", async (req: Request, res: Response) => { return { displayName: profile.displayName, points: profile.points }; }); - return res.status(Constants.SUCCESS).send({ + return res.status(StatusCode.SuccessOK).send({ profiles: filteredLeaderboardEntried, }); }); @@ -124,10 +125,10 @@ profileRouter.get("/", strongJwtVerification, async (_: Request, res: Response) const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: userId }); if (!user) { - return res.status(Constants.NOT_FOUND).send({ error: "UserNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); } - return res.status(Constants.SUCCESS).send(user); + return res.status(StatusCode.SuccessOK).send(user); }); /** @@ -171,16 +172,16 @@ profileRouter.get("/id/:USERID", strongJwtVerification, async (req: Request, res // Trying to perform elevated operation (getting someone else's profile without elevated perms) if (!hasElevatedPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "Forbidden" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } const user: AttendeeProfile | null = await Models.AttendeeProfile.findOne({ userId: userId }); if (!user) { - return res.status(Constants.NOT_FOUND).send({ error: "UserNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserNotFound" }); } - return res.status(Constants.SUCCESS).send(user); + return res.status(StatusCode.SuccessOK).send(user); }); profileRouter.get("/id", (_: Request, res: Response) => { @@ -233,13 +234,13 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons profile.userId = payload.id; if (!isValidProfileFormat(profile)) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "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(Constants.FAILURE).send({ error: "UserAlreadyExists" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "UserAlreadyExists" }); } // Create a metadata object, and return it @@ -247,10 +248,10 @@ profileRouter.post("/", strongJwtVerification, async (req: Request, res: Respons const profileMetadata: AttendeeMetadata = new AttendeeMetadata(profile.userId, Constants.DEFAULT_FOOD_WAVE); const newProfile = await Models.AttendeeProfile.create(profile); await Models.AttendeeMetadata.create(profileMetadata); - return res.status(Constants.SUCCESS).send(newProfile); + return res.status(StatusCode.SuccessOK).send(newProfile); } catch (error) { console.error(error); - return res.status(Constants.FAILURE).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } }); @@ -284,7 +285,7 @@ profileRouter.delete("/", strongJwtVerification, async (_: Request, res: Respons ) { return res.status(Constants.NOT_FOUND).send({ success: false, error: "AttendeeNotFound" }); } - return res.status(Constants.SUCCESS).send({ success: true }); + return res.status(StatusCode.SuccessOK).send({ success: true }); }); export default profileRouter; diff --git a/src/services/staff/staff-router.ts b/src/services/staff/staff-router.ts index 5882ac8e..bedb4082 100644 --- a/src/services/staff/staff-router.ts +++ b/src/services/staff/staff-router.ts @@ -9,6 +9,7 @@ import Constants from "../../constants.js"; import { EventMetadata } from "../../database/event-db.js"; import Models from "../../database/models.js"; +import { StatusCode } from "status-code-enum"; const staffRouter: Router = Router(); @@ -47,29 +48,29 @@ staffRouter.post("/attendance/", strongJwtVerification, async (req: Request, res const userId: string = payload.id; // Only staff can mark themselves as attending these events if (!hasStaffPerms(payload)) { - return res.status(Constants.FORBIDDEN).send({ error: "Forbidden" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } if (!eventId) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "InvalidParams" }); } const metadata: EventMetadata | null = await Models.EventMetadata.findOne({ eventId: eventId }); if (!metadata) { - return res.status(Constants.BAD_REQUEST).send({ error: "EventNotFound" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "EventNotFound" }); } const timestamp: number = Math.round(Date.now() / Constants.MILLISECONDS_PER_SECOND); console.log(metadata.exp, timestamp); if (metadata.exp <= timestamp) { - return res.status(Constants.BAD_REQUEST).send({ error: "CodeExpired" }); + return res.status(StatusCode.ClientErrorBadRequest).send({ error: "CodeExpired" }); } await Models.UserAttendance.findOneAndUpdate({ userId: userId }, { $addToSet: { attendance: eventId } }, { upsert: true }); await Models.EventAttendance.findOneAndUpdate({ eventId: eventId }, { $addToSet: { attendees: userId } }, { upsert: true }); - return res.status(Constants.SUCCESS).send({ status: "Success" }); + return res.status(StatusCode.SuccessOK).send({ status: "Success" }); }); export default staffRouter; diff --git a/src/services/user/user-router.test.ts b/src/services/user/user-router.test.ts index 1ba5cdd2..9ea15848 100644 --- a/src/services/user/user-router.test.ts +++ b/src/services/user/user-router.test.ts @@ -1,61 +1,187 @@ -import { describe, expect, it, beforeEach } from "@jest/globals"; -import { TESTER, get, getAsAdmin, getAsAttendee, getAsStaff } from "../../testTools.js"; +import { describe, expect, it, beforeEach, jest } from "@jest/globals"; +import { AUTH_ROLE_TO_ROLES, TESTER, get, getAsAdmin, getAsAttendee, getAsStaff } from "../../testTools.js"; import Models from "../../database/models.js"; +import { UserInfo } from "../../database/user-db.js"; +import { AuthInfo } from "../../database/auth-db.js"; +import * as authLib from "../auth/auth-lib.js"; +import { Role } from "../auth/auth-models.js"; +import Constants from "../../constants.js"; +import { SpiedFunction } from "jest-mock"; +import { StatusCode } from "status-code-enum"; -// Before each test, add the tester to the user model +const TESTER_USER = { + userId: TESTER.id, + name: TESTER.name, + email: TESTER.email, +} satisfies UserInfo; + +const OTHER_USER = { + userId: "other-user", + email: `other-user@hackillinois.org`, + name: "Other User", +} satisfies UserInfo; + +const OTHER_USER_AUTH = { + userId: OTHER_USER.userId, + provider: "github", + roles: AUTH_ROLE_TO_ROLES[Role.ATTENDEE], +} satisfies AuthInfo; + +// Before each test, initialize database with tester & other users beforeEach(async () => { Models.initialize(); - await Models.UserInfo.create({ - userId: TESTER.id, - name: TESTER.name, - email: TESTER.email, + await Models.UserInfo.create(TESTER_USER); + await Models.UserInfo.create(OTHER_USER); + await Models.AuthInfo.create(OTHER_USER_AUTH); +}); + +/* + * Mocks generateJwtToken with a wrapper so calls and returns can be examined. Does not change behavior. + */ +function mockGenerateJwtTokenWithWrapper(): SpiedFunction { + const mockedAuthLib = require("../auth/auth-lib.js") as typeof authLib; + const mockedGenerateJwtToken = jest.spyOn(mockedAuthLib, "generateJwtToken"); + mockedGenerateJwtToken.mockImplementation((payload, shouldNotExpire, expiration) => { + return authLib.generateJwtToken(payload, shouldNotExpire, expiration); + }); + return mockedGenerateJwtToken; +} + +describe("GET /user/qr/", () => { + it("works for a attendee", async () => { + const mockedGenerateJwtToken = mockGenerateJwtTokenWithWrapper(); + + const response = await getAsAttendee("/user/qr/").expect(StatusCode.SuccessOK); + + const jwtReturned = mockedGenerateJwtToken.mock.results[mockedGenerateJwtToken.mock.results.length - 1]!.value; + + expect(JSON.parse(response.text)).toMatchObject({ + userId: TESTER_USER.userId, + qrInfo: `hackillinois://user?userToken=${jwtReturned}`, + }); + + expect(mockedGenerateJwtToken).toBeCalledWith( + expect.objectContaining({ + id: TESTER_USER.userId, + }), + false, + Constants.QR_EXPIRY_TIME, + ); + }); +}); + +describe("GET /user/qr/:USERID/", () => { + it("gives a forbidden error for a non-staff user", async () => { + const response = await getAsAttendee(`/user/qr/${OTHER_USER.userId}/`).expect(StatusCode.ClientErrorForbidden); + + expect(JSON.parse(response.text)).toHaveProperty("error", "Forbidden"); + }); + + it("works for a non-staff user requesting their qr code", async () => { + const mockedGenerateJwtToken = mockGenerateJwtTokenWithWrapper(); + + const response = await getAsAttendee(`/user/qr/${TESTER_USER.userId}/`).expect(StatusCode.SuccessOK); + + const jwtReturned = mockedGenerateJwtToken.mock.results[mockedGenerateJwtToken.mock.results.length - 1]!.value; + + expect(JSON.parse(response.text)).toMatchObject({ + userId: TESTER_USER.userId, + qrInfo: `hackillinois://user?userToken=${jwtReturned}`, + }); + + expect(mockedGenerateJwtToken).toBeCalledWith( + expect.objectContaining({ + id: TESTER_USER.userId, + }), + false, + Constants.QR_EXPIRY_TIME, + ); + }); + + it("works for a staff user", async () => { + const mockedGenerateJwtToken = mockGenerateJwtTokenWithWrapper(); + + const response = await getAsStaff(`/user/qr/${OTHER_USER.userId}/`).expect(StatusCode.SuccessOK); + + const jwtReturned = mockedGenerateJwtToken.mock.results[mockedGenerateJwtToken.mock.results.length - 1]!.value; + + expect(JSON.parse(response.text)).toMatchObject({ + userId: OTHER_USER.userId, + qrInfo: `hackillinois://user?userToken=${jwtReturned}`, + }); + + expect(mockedGenerateJwtToken).toBeCalledWith( + expect.objectContaining({ + id: OTHER_USER.userId, + }), + false, + Constants.QR_EXPIRY_TIME, + ); }); }); -describe("GET /", () => { +describe("GET /user/", () => { it("gives an unauthorized error for an unauthenticated user", async () => { - const response = await get("/user/").expect(401); + const response = await get("/user/").expect(StatusCode.ClientErrorUnauthorized); expect(JSON.parse(response.text)).toHaveProperty("error", "NoToken"); }); it("gives an not found error for an non-existent user", async () => { await Models.UserInfo.deleteOne({ - userId: TESTER.id, + userId: TESTER_USER.userId, }); - const response = await getAsAttendee("/user/").expect(400); + const response = await getAsAttendee("/user/").expect(StatusCode.ClientErrorNotFound); expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound"); }); it("works for an attendee user", async () => { - const response = await getAsAttendee("/user/").expect(200); + const response = await getAsAttendee("/user/").expect(StatusCode.SuccessOK); - expect(JSON.parse(response.text)).toMatchObject({ - userId: TESTER.id, - name: TESTER.name, - email: TESTER.email, - }); + expect(JSON.parse(response.text)).toMatchObject(TESTER_USER); }); it("works for an staff user", async () => { - const response = await getAsStaff("/user/").expect(200); + const response = await getAsStaff("/user/").expect(StatusCode.SuccessOK); - expect(JSON.parse(response.text)).toMatchObject({ - userId: TESTER.id, - name: TESTER.name, - email: TESTER.email, - }); + expect(JSON.parse(response.text)).toMatchObject(TESTER_USER); }); it("works for an admin user", async () => { - const response = await getAsAdmin("/user/").expect(200); + const response = await getAsAdmin("/user/").expect(StatusCode.SuccessOK); - expect(JSON.parse(response.text)).toMatchObject({ - userId: TESTER.id, - name: TESTER.name, - email: TESTER.email, + expect(JSON.parse(response.text)).toMatchObject(TESTER_USER); + }); +}); + +describe("GET /user/:USERID/", () => { + it("gives an forbidden error for a non-staff user", async () => { + const response = await getAsAttendee(`/user/${OTHER_USER.userId}/`).expect(StatusCode.ClientErrorForbidden); + + expect(JSON.parse(response.text)).toHaveProperty("error", "Forbidden"); + }); + + it("gives an not found error for a non-existent user", async () => { + await Models.UserInfo.deleteOne({ + userId: OTHER_USER.userId, }); + + const response = await getAsStaff(`/user/${OTHER_USER.userId}/`).expect(StatusCode.ClientErrorNotFound); + + expect(JSON.parse(response.text)).toHaveProperty("error", "UserNotFound"); + }); + + it("works for a non-staff user requesting themselves", async () => { + const response = await getAsAttendee(`/user/${TESTER_USER.userId}/`).expect(StatusCode.SuccessOK); + + expect(JSON.parse(response.text)).toMatchObject(TESTER_USER); + }); + + it("works for a staff user", async () => { + const response = await getAsStaff(`/user/${OTHER_USER.userId}/`).expect(StatusCode.SuccessOK); + + expect(JSON.parse(response.text)).toMatchObject(OTHER_USER); }); }); diff --git a/src/services/user/user-router.ts b/src/services/user/user-router.ts index 704eade7..cb3ea5f3 100644 --- a/src/services/user/user-router.ts +++ b/src/services/user/user-router.ts @@ -1,14 +1,14 @@ import { Router, Request, Response } from "express"; +import { StatusCode } from "status-code-enum"; -import Constants from "../../constants.js"; import { strongJwtVerification } from "../../middleware/verify-jwt.js"; import { JwtPayload } from "../auth/auth-models.js"; import { generateJwtToken, getJwtPayloadFromDB, hasElevatedPerms, hasStaffPerms } from "../auth/auth-lib.js"; -import { UserFormat, isValidUserFormat } from "./user-formats.js"; import { UserInfo } from "../../database/user-db.js"; import Models from "../../database/models.js"; +import Constants from "../../constants.js"; const userRouter: Router = Router(); @@ -33,9 +33,9 @@ const userRouter: Router = Router(); userRouter.get("/qr/", strongJwtVerification, (_: Request, res: Response) => { // Return the same payload, but with a shorter expiration time const payload: JwtPayload = res.locals.payload as JwtPayload; - const token: string = generateJwtToken(payload, false, "20s"); + const token: string = generateJwtToken(payload, false, Constants.QR_EXPIRY_TIME); const uri: string = `hackillinois://user?userToken=${token}`; - res.status(Constants.SUCCESS).send({ userId: payload.id, qrInfo: uri }); + res.status(StatusCode.SuccessOK).send({ userId: payload.id, qrInfo: uri }); }); /** @@ -63,11 +63,6 @@ userRouter.get("/qr/", strongJwtVerification, (_: Request, res: Response) => { userRouter.get("/qr/:USERID", strongJwtVerification, async (req: Request, res: Response) => { const targetUser: string | undefined = req.params.USERID as string; - // If target user -> redirect to base function - if (!targetUser) { - return res.redirect("/user/qr/"); - } - const payload: JwtPayload = res.locals.payload as JwtPayload; let newPayload: JwtPayload | undefined; @@ -81,13 +76,13 @@ 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(Constants.FORBIDDEN).send("Forbidden"); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); } // Generate the token const token: string = generateJwtToken(newPayload, false, "20s"); const uri: string = `hackillinois://user?userToken=${token}`; - return res.status(Constants.SUCCESS).send({ userId: payload.id, qrInfo: uri }); + return res.status(StatusCode.SuccessOK).send({ userId: newPayload.id, qrInfo: uri }); }); /** @@ -113,11 +108,6 @@ userRouter.get("/qr/:USERID", strongJwtVerification, async (req: Request, res: R * @apiUse strongVerifyErrors */ userRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Response) => { - // If no target user, exact same as next route - if (!req.params.USERID) { - return res.redirect("/"); - } - const targetUser: string = req.params.USERID ?? ""; // Get payload, and check if authorized @@ -126,13 +116,13 @@ userRouter.get("/:USERID", strongJwtVerification, async (req: Request, res: Resp // Authorized -> return the user object const userInfo: UserInfo | null = await Models.UserInfo.findOne({ userId: targetUser }); if (userInfo) { - return res.status(Constants.SUCCESS).send(userInfo); + return res.status(StatusCode.SuccessOK).send(userInfo); } else { - return res.status(Constants.INTERNAL_ERROR).send({ error: "UserNotFound" }); + return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" }); } } - return res.status(Constants.FORBIDDEN).send({ error: "Forbidden" }); + return res.status(StatusCode.ClientErrorForbidden).send({ error: "Forbidden" }); }); /** @@ -161,65 +151,9 @@ userRouter.get("/", strongJwtVerification, async (_: Request, res: Response) => const user: UserInfo | null = await Models.UserInfo.findOne({ userId: payload.id }); if (user) { - return res.status(Constants.SUCCESS).send(user); - } else { - return res.status(Constants.BAD_REQUEST).send({ error: "UserNotFound" }); - } -}); - -/** - * @api {post} /user/ POST /user/ - * @apiGroup User - * @apiDescription Update a given user - * - * @apiBody {String} userId UserID - * @apiBody {String} name User's name. - * @apiBody {String} email Email address (staff gmail or Github email). - * @apiParamExample {json} Example Request: - * { - "userId": "provider00001", - "name": "john doe", - "email": "johndoe@provider.com" - * } - * - * @apiSuccess (200: Success) {String} userId UserID - * @apiSuccess (200: Success) {String} name User's name. - * @apiSuccess (200: Success) {String} email Email address (staff gmail or Github email). - - * @apiSuccessExample Example Success Response: - * HTTP/1.1 200 OK - * { - "userId": "provider00001", - "name": "john", - "email": "johndoe@provider.com" - * } - * @apiUse strongVerifyErrors - */ -userRouter.post("/", strongJwtVerification, async (req: Request, res: Response) => { - const token: JwtPayload = res.locals.payload as JwtPayload; - - if (!hasElevatedPerms(token)) { - return res.status(Constants.FORBIDDEN).send({ error: "InvalidToken" }); - } - - // Get userData from the request, and print to output - const userData: UserFormat = req.body as UserFormat; - - if (!isValidUserFormat(userData)) { - return res.status(Constants.BAD_REQUEST).send({ error: "InvalidParams" }); - } - - // Update the given user - const updatedUser: UserInfo | null = await Models.UserInfo.findOneAndUpdate( - { userId: userData.userId }, - { $set: userData }, - { upsert: true }, - ); - - if (updatedUser) { - return res.status(Constants.SUCCESS).send(updatedUser); + return res.status(StatusCode.SuccessOK).send(user); } else { - return res.status(Constants.INTERNAL_ERROR).send({ error: "InternalError" }); + return res.status(StatusCode.ClientErrorNotFound).send({ error: "UserNotFound" }); } }); diff --git a/src/services/version/version-router.ts b/src/services/version/version-router.ts index d8ef4bc8..ea686293 100644 --- a/src/services/version/version-router.ts +++ b/src/services/version/version-router.ts @@ -1,7 +1,7 @@ -import Constants from "../../constants.js"; import { Router } from "express"; import { Request, Response } from "express-serve-static-core"; import { ConfigReader } from "../../middleware/config-reader.js"; +import { StatusCode } from "status-code-enum"; const versionRouter: Router = Router(); @@ -20,7 +20,7 @@ const versionRouter: Router = Router(); versionRouter.get("/android/", (_: Request, res: Response) => { const androidVersion: string = ConfigReader.androidVersion; - res.status(Constants.SUCCESS).send({ version: androidVersion }); + res.status(StatusCode.SuccessOK).send({ version: androidVersion }); }); /** @@ -37,7 +37,7 @@ versionRouter.get("/android/", (_: Request, res: Response) => { */ versionRouter.get("/ios/", (_: Request, res: Response) => { const iosVersion: string = ConfigReader.iosVersion; - res.status(Constants.SUCCESS).send({ version: iosVersion }); + res.status(StatusCode.SuccessOK).send({ version: iosVersion }); }); export default versionRouter; diff --git a/src/testTools.ts b/src/testTools.ts index 09d50838..dd46c639 100644 --- a/src/testTools.ts +++ b/src/testTools.ts @@ -14,7 +14,7 @@ export const TESTER = { }; // A mapping of role to roles they have, used for JWT generation -const AUTH_ROLE_TO_ROLES: Record = { +export const AUTH_ROLE_TO_ROLES: Record = { [Role.USER]: [Role.USER], [Role.APPLICANT]: [Role.USER, Role.APPLICANT], [Role.ATTENDEE]: [Role.USER, Role.APPLICANT, Role.ATTENDEE], @@ -28,7 +28,10 @@ const AUTH_ROLE_TO_ROLES: Record = { [Role.BLOBSTORE]: [Role.BLOBSTORE], }; -function setAuth(request: request.Test, role?: Role): request.Test { +/* + * Set auth header & misc headers + */ +function setHeaders(request: request.Test, role?: Role): request.Test { if (!role) { return request; } @@ -45,7 +48,10 @@ function setAuth(request: request.Test, role?: Role): request.Test { roles: AUTH_ROLE_TO_ROLES[role], }); - return request.set("Authorization", jwt as string); + return request + .set("Authorization", jwt as string) + .set("Accept", "application/json") + .set("Content-Type", "application/json"); } // Dynamically require app so it's always the freshest version @@ -57,19 +63,19 @@ function app(): Express.Application { } export function get(url: string, role?: Role): request.Test { - return setAuth(request(app()).get(url), role); + return setHeaders(request(app()).get(url), role); } export function post(url: string, role?: Role): request.Test { - return setAuth(request(app()).post(url), role); + return setHeaders(request(app()).post(url), role); } export function put(url: string, role?: Role): request.Test { - return setAuth(request(app()).put(url), role); + return setHeaders(request(app()).put(url), role); } export function del(url: string, role?: Role): request.Test { - return setAuth(request(app()).delete(url), role); + return setHeaders(request(app()).delete(url), role); } // Helpers that are nicer to use diff --git a/yarn.lock b/yarn.lock index bf135c83..3a4a1a1b 100644 --- a/yarn.lock +++ b/yarn.lock @@ -5874,6 +5874,11 @@ stat-mode@0.3.0: resolved "https://registry.yarnpkg.com/stat-mode/-/stat-mode-0.3.0.tgz#69283b081f851582b328d2a4ace5f591ce52f54b" integrity sha512-QjMLR0A3WwFY2aZdV0okfFEJB5TRjkggXZjxP3A1RsWsNHNu3YPv8btmtc6iCFZ0Rul3FE93OYogvhOUClU+ng== +status-code-enum@^1.0.0: + version "1.0.0" + resolved "https://registry.yarnpkg.com/status-code-enum/-/status-code-enum-1.0.0.tgz#097b6d87e8402fa20f5fbf3c50a24eda20d4efed" + integrity sha512-aDTkL2wug8wYX8i0a2K1foqIDaJyGF/a3I0KTZ9gD4MJaxzd7/LKD1VQ2rXL5u7fWESVY+qY2LQLqj6LK/YXiA== + statuses@2.0.1: version "2.0.1" resolved "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz"