/* * Copyright (C) 2019 StApps * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Affero General Public License as * published by the Free Software Foundation, either version 3 of the * License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Affero General Public License for more details. * * You should have received a copy of the GNU Affero General Public License * along with this program. If not, see . */ import { SCInternalServerErrorResponse, SCMethodNotAllowedErrorResponse, SCRoute, SCValidationErrorResponse, } from '@openstapps/core'; import {Application, Router} from 'express'; import PromiseRouter from 'express-promise-router'; import {ValidationError} from 'jsonschema'; import {isTestEnvironment, logger, validator} from '../common'; import {isHttpMethod} from './HTTPTypes'; /** * Creates a router from a route class (model of a route) and a handler function which implements the logic * * The given router performs a request and respone validation, sets status codes and checks if the given handler * only returns errors that are allowed for the client to see * * @param routeClass * @param handler */ export function createRoute( routeClass: SCRoute, handler: (validatedBody: any, app: Application, params?: { [parameterName: string]: string }) => Promise, ): Router { // create router const router = PromiseRouter({mergeParams: true}); // create route // the given type has no index signature so we have to cast to get the IRouteHandler when a HTTP method is given const route = router.route(routeClass.urlFragment); // get route parameters (path parameters) if (Array.isArray(routeClass.obligatoryParameters) && routeClass.obligatoryParameters.length > 0) { routeClass.obligatoryParameters.forEach((parameterName) => { router.param(parameterName, async (req, _res, next, parameterValue: string) => { if (typeof req.params === 'undefined') { req.params = {}; } // set parameter values on request object req.params[parameterName] = parameterValue; // hand over the request to the next handler (our method route handler) next(); }); }); } const verb = routeClass.method.toLowerCase(); // check if route has a valid http verb if (isHttpMethod(verb)) { // create a route handler for the given HTTP method route[verb](async (req, res) => { try { // validate request const requestValidation = validator.validate(req.body, routeClass.requestBodyName); if (requestValidation.errors.length > 0) { const error = new SCValidationErrorResponse( requestValidation.errors, isTestEnvironment, ); res.status(error.statusCode); res.json(error); logger.warn(error); return; } // hand over request to handler with path parameters const response = await handler(req.body, req.app, req.params); // validate response generated by handler const responseErrors: ValidationError[] = validator.validate(response, routeClass.responseBodyName).errors; if (responseErrors.length > 0) { const validationError = new SCValidationErrorResponse( responseErrors, isTestEnvironment, ); // The validation error is not caused by faulty user input, but through an error that originates somewhere in // the backend, therefor we use this "stacked" error. const internalServerError = new SCInternalServerErrorResponse( validationError, isTestEnvironment, ); res.status(internalServerError.statusCode); res.json(internalServerError); logger.warn(internalServerError); return; } // set status code res.status(routeClass.statusCodeSuccess); // respond res.json(response); } catch (error) { // if the error response is allowed on the route if (routeClass.errorNames.some((constructorType) => error instanceof constructorType)) { // respond with the error from the handler res.status(error.statusCode); res.json(error); logger.warn(error); } else { // the error is not allowed so something went wrong const internalServerError = new SCInternalServerErrorResponse( error, isTestEnvironment, ); res.status(internalServerError.statusCode); res.json(internalServerError); logger.error(error); } } }); } else { throw new Error('Invalid HTTP verb in route definition. Please check route definitions in `@openstapps/core`'); } // return a SCMethodNotAllowedErrorResponse on all other HTTP methods route.all((_req, res) => { const error = new SCMethodNotAllowedErrorResponse(isTestEnvironment); res.status(error.statusCode); res.json(error); logger.warn(error); }); // return router return router; }