Skip to content

Commit

Permalink
Add role and description support to specification
Browse files Browse the repository at this point in the history
  • Loading branch information
Timothy-Gonzalez committed Oct 8, 2024
1 parent 7c1824f commit bd18db7
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 4 deletions.
9 changes: 9 additions & 0 deletions jest.presetup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand All @@ -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;
});
41 changes: 38 additions & 3 deletions src/middleware/specification.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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"];

Expand All @@ -21,6 +24,7 @@ export enum Tag {
S3 = "S3",
SHOP = "Shop",
STAFF = "Staff",
USER = "User",
VERSION = "Version",
}

Expand All @@ -36,7 +40,9 @@ export interface Specification<Params = AnyZodObject, Responses = ResponsesObjec
path: string;
method: Method;
tag: Tag;
role: Role | null;
summary: string;
description?: string;
parameters?: Params;
body?: Body;
responses: Responses;
Expand All @@ -52,10 +58,39 @@ export default function specification<Params extends AnyZodObject, Responses ext
registerPathSpecification(spec);

return async (req: Request, res: Response, next: NextFunction) => {
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,
Expand All @@ -65,13 +100,13 @@ export default function specification<Params extends AnyZodObject, Responses ext
if (spec.body) {
const result = await spec.body.safeParseAsync(req.body);
if (!result.success) {
res.status(StatusCode.ClientErrorBadRequest).json({
return res.status(StatusCode.ClientErrorBadRequest).json({
error: "BadRequest",
message: "Bad request made - invalid body format",
validationErrors: result.error.errors,
});
}
}
next();
return next();
};
}
2 changes: 2 additions & 0 deletions src/middleware/verify-jwt.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@ import jsonwebtoken from "jsonwebtoken";
import { StatusCode } from "status-code-enum";
import Config from "../config";

// TODO: Remove all usages of these

/**
* @apiDefine strongVerifyErrors
* @apiHeader {String} Authorization JWT token.
Expand Down
25 changes: 24 additions & 1 deletion src/openapi.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,13 @@ import { ResponsesObject, Specification } from "./middleware/specification";
let openAPISpec: OpenAPIObject | undefined = undefined;
export const Registry = new OpenAPIRegistry();

// Security component
const bearerAuth = Registry.registerComponent("securitySchemes", "bearerAuth", {
type: "http",
scheme: "bearer",
bearerFormat: "jwt",
});

function generateOpenAPISpec(): OpenAPIObject {
const generator = new OpenApiGeneratorV31(Registry.definitions);
return generator.generateDocument({
Expand Down Expand Up @@ -38,7 +45,21 @@ export function registerPathSpecification<
Responses extends ResponsesObject,
Body extends AnyZodObject,
>(specification: Specification<Params, Responses, Body>): 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)) {
Expand Down Expand Up @@ -66,9 +87,11 @@ export function registerPathSpecification<
Registry.registerPath({
method,
path,
security,
responses,
request,
summary,
description: combinedDescription,
tags: [tag],
});
}
8 changes: 8 additions & 0 deletions src/services/version/version-router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand Down

0 comments on commit bd18db7

Please sign in to comment.