From bd18db7196a9380e7ab306bff6a1f8cbe6ca2090 Mon Sep 17 00:00:00 2001 From: Timothy-Gonzalez <105177619+Timothy-Gonzalez@users.noreply.github.com> Date: Tue, 8 Oct 2024 17:15:53 -0500 Subject: [PATCH] Add role and description support to specification --- jest.presetup.ts | 9 ++++++ src/middleware/specification.ts | 41 ++++++++++++++++++++++++-- src/middleware/verify-jwt.ts | 2 ++ src/openapi.ts | 25 +++++++++++++++- src/services/version/version-router.ts | 8 +++++ 5 files changed, 81 insertions(+), 4 deletions(-) diff --git a/jest.presetup.ts b/jest.presetup.ts index 24e89391..7b3984d9 100644 --- a/jest.presetup.ts +++ b/jest.presetup.ts @@ -2,6 +2,8 @@ import dotenv from "dotenv"; import path from "path"; import { jest } from "@jest/globals"; import { readFileSync } from "fs"; +import { extendZodWithOpenApi } from "@asteasolutions/zod-to-openapi"; +import type zodType from "zod"; // Mock the env loading to load from .test.env instead jest.mock("./src/env.js", () => { @@ -13,3 +15,10 @@ jest.mock("./src/env.js", () => { __esModule: true, }; }); + +// Mock extended zod since types.ts doesn't work for some reason +jest.mock("zod", () => { + const zod = jest.requireActual("zod"); + extendZodWithOpenApi(zod as typeof zodType); + return zod; +}); diff --git a/src/middleware/specification.ts b/src/middleware/specification.ts index e30213e8..6d12c3bd 100644 --- a/src/middleware/specification.ts +++ b/src/middleware/specification.ts @@ -4,6 +4,9 @@ import StatusCode from "status-code-enum"; import { Response, Request, NextFunction } from "express"; import { registerPathSpecification } from "../openapi"; import { RouteConfig } from "@asteasolutions/zod-to-openapi"; +import { Role } from "../services/auth/auth-models"; +import { decodeJwtToken } from "../services/auth/auth-lib"; +import { TokenExpiredError } from "jsonwebtoken"; export type Method = RouteConfig["method"]; @@ -21,6 +24,7 @@ export enum Tag { S3 = "S3", SHOP = "Shop", STAFF = "Staff", + USER = "User", VERSION = "Version", } @@ -36,7 +40,9 @@ export interface Specification { + if (spec.role) { + try { + const jwt = decodeJwtToken(req.headers.authorization); + if (!jwt.roles.includes(spec.role)) { + return res.status(StatusCode.ClientErrorForbidden).json({ + error: "Forbidden", + message: `You require the role ${spec.role} to do that`, + }); + } + } catch (error) { + if (error instanceof TokenExpiredError) { + return res.status(StatusCode.ClientErrorForbidden).json({ + error: "TokenExpired", + message: "Your session has expired, please log in again", + }); + } else if (error instanceof Error && error.message == "NoToken") { + return res.status(StatusCode.ClientErrorUnauthorized).send({ + error: "NoToken", + message: "A authorization token must be sent for this request", + }); + } else { + return res.status(StatusCode.ClientErrorUnauthorized).send({ + error: "TokenInvalid", + message: "Your session is invalid, please log in again", + }); + } + } + } + if (spec.parameters) { const result = await spec.parameters.safeParseAsync(req.params); if (!result.success) { - res.status(StatusCode.ClientErrorBadRequest).json({ + return res.status(StatusCode.ClientErrorBadRequest).json({ error: "BadRequest", message: "Bad request made - invalid parameters format", validationErrors: result.error.errors, @@ -65,13 +100,13 @@ export default function specification(specification: Specification): void { - const { method, path, summary, parameters: params, tag } = specification; + const { method, path, tag, role, summary, description, parameters: params } = specification; + const security = role + ? [ + { + [bearerAuth.name]: [role], + }, + ] + : undefined; + const descriptionHeader = role && `**Required role: ${role}**`; + let combinedDescription: string | undefined = undefined; + if (description && descriptionHeader) { + combinedDescription = `${descriptionHeader}\n\n${description}`; + } else { + combinedDescription = descriptionHeader || description; + } const responses: RouteConfig["responses"] = {}; for (const [statusCode, response] of Object.entries(specification.responses)) { @@ -66,9 +87,11 @@ export function registerPathSpecification< Registry.registerPath({ method, path, + security, responses, request, summary, + description: combinedDescription, tags: [tag], }); } diff --git a/src/services/version/version-router.ts b/src/services/version/version-router.ts index 1bf2e3fc..83ba0157 100644 --- a/src/services/version/version-router.ts +++ b/src/services/version/version-router.ts @@ -12,7 +12,11 @@ versionRouter.get( method: "get", path: "/version/android/", tag: Tag.VERSION, + role: null, summary: "Gets the current android version", + description: + "Note that this version is pulled from the adonix-metadata repo " + + "([https://github.com/hackIllinois/adonix-metadata](https://github.com/hackIllinois/adonix-metadata))", responses: { [StatusCode.SuccessOK]: { description: "The current version", @@ -32,7 +36,11 @@ versionRouter.get( method: "get", path: "/version/ios/", tag: Tag.VERSION, + role: null, summary: "Gets the current ios version", + description: + "Note that this version is pulled from the adonix-metadata repo " + + "([https://github.com/hackIllinois/adonix-metadata](https://github.com/hackIllinois/adonix-metadata))", responses: { [StatusCode.SuccessOK]: { description: "The current version",