diff --git a/backend/src/api/routes/admin.ts b/backend/src/api/routes/admin.ts index 9e94b0b50..4c1bdb93f 100644 --- a/backend/src/api/routes/admin.ts +++ b/backend/src/api/routes/admin.ts @@ -1,21 +1,19 @@ // import joi from "joi"; import { Router } from "express"; import { authenticateRequest } from "../../middlewares/auth"; -import { - asyncHandler, - checkIfUserIsAdmin, - validateConfiguration, - validateRequest, -} from "../../middlewares/api-utils"; import * as AdminController from "../controllers/admin"; import { adminLimit } from "../../middlewares/rate-limit"; import { sendForgotPasswordEmail, toggleBan } from "../controllers/user"; import joi from "joi"; +import { validate } from "../../middlewares/configuration"; +import { checkIfUserIsAdmin } from "../../middlewares/permission"; +import { asyncHandler } from "../../middlewares/utility"; +import { validateRequest } from "../../middlewares/validation"; const router = Router(); router.use( - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.admin.endpointsEnabled; }, diff --git a/backend/src/api/routes/ape-keys.ts b/backend/src/api/routes/ape-keys.ts index 0d670e113..1e49bc892 100644 --- a/backend/src/api/routes/ape-keys.ts +++ b/backend/src/api/routes/ape-keys.ts @@ -1,14 +1,12 @@ import joi from "joi"; import { Router } from "express"; -import { - asyncHandler, - checkUserPermissions, - validateConfiguration, - validateRequest, -} from "../../middlewares/api-utils"; import { authenticateRequest } from "../../middlewares/auth"; import * as ApeKeyController from "../controllers/ape-key"; import * as RateLimit from "../../middlewares/rate-limit"; +import { checkUserPermissions } from "../../middlewares/permission"; +import { validate } from "../../middlewares/configuration"; +import { asyncHandler } from "../../middlewares/utility"; +import { validateRequest } from "../../middlewares/validation"; const apeKeyNameSchema = joi .string() @@ -30,7 +28,7 @@ const checkIfUserCanManageApeKeys = checkUserPermissions({ const router = Router(); router.use( - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.apeKeys.endpointsEnabled; }, diff --git a/backend/src/api/routes/configs.ts b/backend/src/api/routes/configs.ts index 205885aef..c2e580b62 100644 --- a/backend/src/api/routes/configs.ts +++ b/backend/src/api/routes/configs.ts @@ -1,9 +1,10 @@ import { Router } from "express"; import { authenticateRequest } from "../../middlewares/auth"; -import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; import configSchema from "../schemas/config-schema"; import * as ConfigController from "../controllers/config"; import * as RateLimit from "../../middlewares/rate-limit"; +import { asyncHandler } from "../../middlewares/utility"; +import { validateRequest } from "../../middlewares/validation"; const router = Router(); diff --git a/backend/src/api/routes/configuration.ts b/backend/src/api/routes/configuration.ts index 17df5bf95..54cc3a963 100644 --- a/backend/src/api/routes/configuration.ts +++ b/backend/src/api/routes/configuration.ts @@ -1,14 +1,11 @@ import joi from "joi"; import { Router } from "express"; -import { - asyncHandler, - checkIfUserIsAdmin, - useInProduction, - validateRequest, -} from "../../middlewares/api-utils"; import * as ConfigurationController from "../controllers/configuration"; import { authenticateRequest } from "../../middlewares/auth"; import { adminLimit } from "../../middlewares/rate-limit"; +import { asyncHandler, useInProduction } from "../../middlewares/utility"; +import { checkIfUserIsAdmin } from "../../middlewares/permission"; +import { validateRequest } from "../../middlewares/validation"; const router = Router(); diff --git a/backend/src/api/routes/dev.ts b/backend/src/api/routes/dev.ts index 2ea6516cf..2c2dd2a42 100644 --- a/backend/src/api/routes/dev.ts +++ b/backend/src/api/routes/dev.ts @@ -1,17 +1,15 @@ import { Router } from "express"; -import { - asyncHandler, - validateConfiguration, - validateRequest, -} from "../../middlewares/api-utils"; import joi from "joi"; import { createTestData } from "../controllers/dev"; import { isDevEnvironment } from "../../utils/misc"; +import { validate } from "../../middlewares/configuration"; +import { validateRequest } from "../../middlewares/validation"; +import { asyncHandler } from "../../middlewares/utility"; const router = Router(); router.use( - validateConfiguration({ + validate({ criteria: () => { return isDevEnvironment(); }, diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index 44c92c133..57c613896 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -15,7 +15,7 @@ import configuration from "./configuration"; import { version } from "../../version"; import leaderboards from "./leaderboards"; import addSwaggerMiddlewares from "./swagger"; -import { asyncHandler } from "../../middlewares/api-utils"; +import { asyncHandler } from "../../middlewares/utility"; import { MonkeyResponse } from "../../utils/monkey-response"; import { recordClientVersion } from "../../utils/prometheus"; import { diff --git a/backend/src/api/routes/leaderboards.ts b/backend/src/api/routes/leaderboards.ts index 0624bc481..9b2705e3a 100644 --- a/backend/src/api/routes/leaderboards.ts +++ b/backend/src/api/routes/leaderboards.ts @@ -4,11 +4,9 @@ import * as RateLimit from "../../middlewares/rate-limit"; import { withApeRateLimiter } from "../../middlewares/ape-rate-limit"; import { authenticateRequest } from "../../middlewares/auth"; import * as LeaderboardController from "../controllers/leaderboard"; -import { - asyncHandler, - validateRequest, - validateConfiguration, -} from "../../middlewares/api-utils"; +import { validate } from "../../middlewares/configuration"; +import { validateRequest } from "../../middlewares/validation"; +import { asyncHandler } from "../../middlewares/utility"; const BASE_LEADERBOARD_VALIDATION_SCHEMA = { language: joi @@ -39,7 +37,7 @@ const DAILY_LEADERBOARD_VALIDATION_SCHEMA = { const router = Router(); -const requireDailyLeaderboardsEnabled = validateConfiguration({ +const requireDailyLeaderboardsEnabled = validate({ criteria: (configuration) => { return configuration.dailyLeaderboards.enabled; }, @@ -98,7 +96,7 @@ const WEEKLY_XP_LEADERBOARD_VALIDATION_SCHEMA = { weeksBefore: joi.number().min(1).max(1), }; -const requireWeeklyXpLeaderboardEnabled = validateConfiguration({ +const requireWeeklyXpLeaderboardEnabled = validate({ criteria: (configuration) => { return configuration.leaderboards.weeklyXp.enabled; }, diff --git a/backend/src/api/routes/presets.ts b/backend/src/api/routes/presets.ts index c8e99d457..c33ce3afe 100644 --- a/backend/src/api/routes/presets.ts +++ b/backend/src/api/routes/presets.ts @@ -3,8 +3,9 @@ import { authenticateRequest } from "../../middlewares/auth"; import * as PresetController from "../controllers/preset"; import * as RateLimit from "../../middlewares/rate-limit"; import configSchema from "../schemas/config-schema"; -import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; import { Router } from "express"; +import { asyncHandler } from "../../middlewares/utility"; +import { validateRequest } from "../../middlewares/validation"; const router = Router(); diff --git a/backend/src/api/routes/psas.ts b/backend/src/api/routes/psas.ts index 10a228949..6fbbd6759 100644 --- a/backend/src/api/routes/psas.ts +++ b/backend/src/api/routes/psas.ts @@ -1,7 +1,7 @@ import { Router } from "express"; import * as PsaController from "../controllers/psa"; import * as RateLimit from "../../middlewares/rate-limit"; -import { asyncHandler } from "../../middlewares/api-utils"; +import { asyncHandler } from "../../middlewares/utility"; const router = Router(); diff --git a/backend/src/api/routes/public.ts b/backend/src/api/routes/public.ts index 7acc826b9..86491c6b3 100644 --- a/backend/src/api/routes/public.ts +++ b/backend/src/api/routes/public.ts @@ -1,8 +1,9 @@ import { Router } from "express"; import * as PublicController from "../controllers/public"; import * as RateLimit from "../../middlewares/rate-limit"; -import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; +import { asyncHandler } from "../../middlewares/utility"; import joi from "joi"; +import { validateRequest } from "../../middlewares/validation"; const GET_MODE_STATS_VALIDATION_SCHEMA = { language: joi diff --git a/backend/src/api/routes/quotes.ts b/backend/src/api/routes/quotes.ts index dc26d4deb..19a4f7604 100644 --- a/backend/src/api/routes/quotes.ts +++ b/backend/src/api/routes/quotes.ts @@ -3,12 +3,10 @@ import { authenticateRequest } from "../../middlewares/auth"; import { Router } from "express"; import * as QuoteController from "../controllers/quote"; import * as RateLimit from "../../middlewares/rate-limit"; -import { - asyncHandler, - checkUserPermissions, - validateConfiguration, - validateRequest, -} from "../../middlewares/api-utils"; +import { checkUserPermissions } from "../../middlewares/permission"; +import { asyncHandler } from "../../middlewares/utility"; +import { validate } from "../../middlewares/configuration"; +import { validateRequest } from "../../middlewares/validation"; const router = Router(); @@ -40,7 +38,7 @@ router.get( router.post( "/", - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.quotes.submissionsEnabled; }, @@ -140,7 +138,7 @@ const withCustomMessages = joi.string().messages({ router.post( "/report", - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.quotes.reporting.enabled; }, diff --git a/backend/src/api/routes/results.ts b/backend/src/api/routes/results.ts index 1d147b7ad..92328c072 100644 --- a/backend/src/api/routes/results.ts +++ b/backend/src/api/routes/results.ts @@ -1,15 +1,13 @@ import * as ResultController from "../controllers/result"; import resultSchema from "../schemas/result-schema"; -import { - asyncHandler, - validateRequest, - validateConfiguration, -} from "../../middlewares/api-utils"; import * as RateLimit from "../../middlewares/rate-limit"; import { Router } from "express"; import { authenticateRequest } from "../../middlewares/auth"; import joi from "joi"; import { withApeRateLimiter } from "../../middlewares/ape-rate-limit"; +import { validateRequest } from "../../middlewares/validation"; +import { asyncHandler } from "../../middlewares/utility"; +import { validate } from "../../middlewares/configuration"; const router = Router(); @@ -31,7 +29,7 @@ router.get( router.post( "/", - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.results.savingEnabled; }, diff --git a/backend/src/api/routes/users.ts b/backend/src/api/routes/users.ts index bce4515ac..79e5d0658 100644 --- a/backend/src/api/routes/users.ts +++ b/backend/src/api/routes/users.ts @@ -2,16 +2,14 @@ import joi from "joi"; import { authenticateRequest } from "../../middlewares/auth"; import { Router } from "express"; import * as UserController from "../controllers/user"; -import { - asyncHandler, - validateRequest, - validateConfiguration, - checkUserPermissions, -} from "../../middlewares/api-utils"; import * as RateLimit from "../../middlewares/rate-limit"; import { withApeRateLimiter } from "../../middlewares/ape-rate-limit"; import { containsProfanity, isUsernameValid } from "../../utils/validation"; import filterSchema from "../schemas/filter-schema"; +import { asyncHandler } from "../../middlewares/utility"; +import { validate } from "../../middlewares/configuration"; +import { validateRequest } from "../../middlewares/validation"; +import { checkUserPermissions } from "../../middlewares/permission"; const router = Router(); @@ -103,7 +101,7 @@ router.get( router.post( "/signup", - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.users.signUp; }, @@ -243,7 +241,7 @@ router.post( asyncHandler(UserController.optOutOfLeaderboards) ); -const requireFilterPresetsEnabled = validateConfiguration({ +const requireFilterPresetsEnabled = validate({ criteria: (configuration) => { return configuration.results.filterPresets.enabled; }, @@ -389,7 +387,7 @@ router.patch( asyncHandler(UserController.editCustomTheme) ); -const requireDiscordIntegrationEnabled = validateConfiguration({ +const requireDiscordIntegrationEnabled = validate({ criteria: (configuration) => { return configuration.users.discordIntegration.enabled; }, @@ -498,7 +496,7 @@ router.delete( asyncHandler(UserController.removeFavoriteQuote) ); -const requireProfilesEnabled = validateConfiguration({ +const requireProfilesEnabled = validate({ criteria: (configuration) => { return configuration.users.profiles.enabled; }, @@ -577,7 +575,7 @@ router.patch( const mailIdSchema = joi.array().items(joi.string().guid()).min(1).default([]); -const requireInboxEnabled = validateConfiguration({ +const requireInboxEnabled = validate({ criteria: (configuration) => { return configuration.users.inbox.enabled; }, @@ -612,7 +610,7 @@ const withCustomMessages = joi.string().messages({ router.post( "/report", - validateConfiguration({ + validate({ criteria: (configuration) => { return configuration.quotes.reporting.enabled; }, diff --git a/backend/src/api/routes/webhooks.ts b/backend/src/api/routes/webhooks.ts index f594009fb..d0e774d09 100644 --- a/backend/src/api/routes/webhooks.ts +++ b/backend/src/api/routes/webhooks.ts @@ -1,7 +1,7 @@ // import joi from "joi"; import { Router } from "express"; import { authenticateGithubWebhook } from "../../middlewares/auth"; -import { asyncHandler } from "../../middlewares/api-utils"; +import { asyncHandler } from "../../middlewares/utility"; import { webhookLimit } from "../../middlewares/rate-limit"; import { githubRelease } from "../controllers/webhooks"; diff --git a/backend/src/middlewares/api-utils.ts b/backend/src/middlewares/api-utils.ts deleted file mode 100644 index 347425a5f..000000000 --- a/backend/src/middlewares/api-utils.ts +++ /dev/null @@ -1,209 +0,0 @@ -import _ from "lodash"; -import joi from "joi"; -import MonkeyError from "../utils/error"; -import { Response, NextFunction, RequestHandler } from "express"; -import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; -import { getUser } from "../dal/user"; -import { isAdmin } from "../dal/admin-uids"; -import { isDevEnvironment } from "../utils/misc"; - -type ValidationOptions = { - criteria: (data: T) => boolean; - invalidMessage?: string; -}; - -const emptyMiddleware = ( - _req: MonkeyTypes.Request, - _res: Response, - next: NextFunction -): void => next(); - -/** - * This utility checks that the server's configuration matches - * the criteria. - */ -function validateConfiguration( - options: ValidationOptions -): RequestHandler { - const { - criteria, - invalidMessage = "This service is currently unavailable.", - } = options; - - return (req: MonkeyTypes.Request, _res: Response, next: NextFunction) => { - const configuration = req.ctx.configuration; - - const validated = criteria(configuration); - if (!validated) { - throw new MonkeyError(503, invalidMessage); - } - - next(); - }; -} - -/** - * Check if the user is an admin before handling request. - * Note that this middleware must be used after authentication in the middleware stack. - */ -function checkIfUserIsAdmin(): RequestHandler { - return async ( - req: MonkeyTypes.Request, - _res: Response, - next: NextFunction - ) => { - try { - const { uid } = req.ctx.decodedToken; - const admin = await isAdmin(uid); - - if (!admin) { - throw new MonkeyError(403, "You don't have permission to do this."); - } - } catch (error) { - next(error); - } - - next(); - }; -} - -/** - * Check user permissions before handling request. - * Note that this middleware must be used after authentication in the middleware stack. - */ -function checkUserPermissions( - options: ValidationOptions -): RequestHandler { - const { criteria, invalidMessage = "You don't have permission to do this." } = - options; - - return async ( - req: MonkeyTypes.Request, - _res: Response, - next: NextFunction - ) => { - try { - const { uid } = req.ctx.decodedToken; - - const userData = await getUser(uid, "check user permissions"); - const hasPermission = criteria(userData); - - if (!hasPermission) { - throw new MonkeyError(403, invalidMessage); - } - } catch (error) { - next(error); - } - - next(); - }; -} - -type AsyncHandler = ( - req: MonkeyTypes.Request, - res?: Response -) => Promise; - -/** - * This utility serves as an alternative to wrapping express handlers with try/catch statements. - * Any routes that use an async handler function should wrap the handler with this function. - * Without this, any errors thrown will not be caught by the error handling middleware, and - * the app will hang! - */ -function asyncHandler(handler: AsyncHandler): RequestHandler { - return async ( - req: MonkeyTypes.Request, - res: Response, - next: NextFunction - ) => { - try { - const handlerData = await handler(req, res); - return handleMonkeyResponse(handlerData, res); - } catch (error) { - next(error); - } - }; -} - -type ValidationSchema = { - body?: object; - query?: object; - params?: object; - headers?: object; -}; - -type ValidationSchemaOption = { - allowUnknown?: boolean; -}; - -type ValidationHandlingOptions = { - validationErrorMessage?: string; -}; - -type ValidationSchemaOptions = { - [schema in keyof ValidationSchema]?: ValidationSchemaOption; -} & ValidationHandlingOptions; - -const VALIDATION_SCHEMA_DEFAULT_OPTIONS: ValidationSchemaOptions = { - body: { allowUnknown: false }, - headers: { allowUnknown: true }, - params: { allowUnknown: false }, - query: { allowUnknown: false }, -}; - -function validateRequest( - validationSchema: ValidationSchema, - validationOptions: ValidationSchemaOptions = VALIDATION_SCHEMA_DEFAULT_OPTIONS -): RequestHandler { - const options = { - ...VALIDATION_SCHEMA_DEFAULT_OPTIONS, - ...validationOptions, - }; - const { validationErrorMessage } = options; - const normalizedValidationSchema: ValidationSchema = _.omit( - validationSchema, - "validationErrorMessage" - ); - - return (req: MonkeyTypes.Request, _res: Response, next: NextFunction) => { - _.each( - normalizedValidationSchema, - (schema: object, key: keyof ValidationSchema) => { - const joiSchema = joi - .object() - .keys(schema) - .unknown(options[key]?.allowUnknown); - - const { error } = joiSchema.validate(req[key] ?? {}); - if (error) { - const errorMessage = error.details[0]?.message; - throw new MonkeyError( - 422, - validationErrorMessage ?? - `${errorMessage} (${error.details[0]?.context?.value})` - ); - } - } - ); - - next(); - }; -} - -/** - * Uses the middlewares only in production. Otherwise, uses an empty middleware. - */ -function useInProduction(middlewares: RequestHandler[]): RequestHandler[] { - return middlewares.map((middleware) => - isDevEnvironment() ? emptyMiddleware : middleware - ); -} - -export { - validateConfiguration, - checkUserPermissions, - checkIfUserIsAdmin, - asyncHandler, - validateRequest, - useInProduction, -}; diff --git a/backend/src/middlewares/configuration.ts b/backend/src/middlewares/configuration.ts new file mode 100644 index 000000000..19ee0b86f --- /dev/null +++ b/backend/src/middlewares/configuration.ts @@ -0,0 +1,31 @@ +import { Response, NextFunction, RequestHandler } from "express"; +import MonkeyError from "../utils/error"; + +export type ValidationOptions = { + criteria: (data: T) => boolean; + invalidMessage?: string; +}; + +/** + * This utility checks that the server's configuration matches + * the criteria. + */ +export function validate( + options: ValidationOptions +): RequestHandler { + const { + criteria, + invalidMessage = "This service is currently unavailable.", + } = options; + + return (req: MonkeyTypes.Request, _res: Response, next: NextFunction) => { + const configuration = req.ctx.configuration; + + const validated = criteria(configuration); + if (!validated) { + throw new MonkeyError(503, invalidMessage); + } + + next(); + }; +} diff --git a/backend/src/middlewares/permission.ts b/backend/src/middlewares/permission.ts new file mode 100644 index 000000000..2a01a9f74 --- /dev/null +++ b/backend/src/middlewares/permission.ts @@ -0,0 +1,63 @@ +import _ from "lodash"; +import MonkeyError from "../utils/error"; +import { Response, NextFunction, RequestHandler } from "express"; +import { getUser } from "../dal/user"; +import { isAdmin } from "../dal/admin-uids"; +import { ValidationOptions } from "./configuration"; + +/** + * Check if the user is an admin before handling request. + * Note that this middleware must be used after authentication in the middleware stack. + */ +export function checkIfUserIsAdmin(): RequestHandler { + return async ( + req: MonkeyTypes.Request, + _res: Response, + next: NextFunction + ) => { + try { + const { uid } = req.ctx.decodedToken; + const admin = await isAdmin(uid); + + if (!admin) { + throw new MonkeyError(403, "You don't have permission to do this."); + } + } catch (error) { + next(error); + } + + next(); + }; +} + +/** + * Check user permissions before handling request. + * Note that this middleware must be used after authentication in the middleware stack. + */ +export function checkUserPermissions( + options: ValidationOptions +): RequestHandler { + const { criteria, invalidMessage = "You don't have permission to do this." } = + options; + + return async ( + req: MonkeyTypes.Request, + _res: Response, + next: NextFunction + ) => { + try { + const { uid } = req.ctx.decodedToken; + + const userData = await getUser(uid, "check user permissions"); + const hasPermission = criteria(userData); + + if (!hasPermission) { + throw new MonkeyError(403, invalidMessage); + } + } catch (error) { + next(error); + } + + next(); + }; +} diff --git a/backend/src/middlewares/utility.ts b/backend/src/middlewares/utility.ts new file mode 100644 index 000000000..d62bac424 --- /dev/null +++ b/backend/src/middlewares/utility.ts @@ -0,0 +1,47 @@ +import _ from "lodash"; +import { Response, NextFunction, RequestHandler } from "express"; +import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; +import { isDevEnvironment } from "../utils/misc"; + +export const emptyMiddleware = ( + _req: MonkeyTypes.Request, + _res: Response, + next: NextFunction +): void => next(); + +type AsyncHandler = ( + req: MonkeyTypes.Request, + res?: Response +) => Promise; + +/** + * This utility serves as an alternative to wrapping express handlers with try/catch statements. + * Any routes that use an async handler function should wrap the handler with this function. + * Without this, any errors thrown will not be caught by the error handling middleware, and + * the app will hang! + */ +export function asyncHandler(handler: AsyncHandler): RequestHandler { + return async ( + req: MonkeyTypes.Request, + res: Response, + next: NextFunction + ) => { + try { + const handlerData = await handler(req, res); + return handleMonkeyResponse(handlerData, res); + } catch (error) { + next(error); + } + }; +} + +/** + * Uses the middlewares only in production. Otherwise, uses an empty middleware. + */ +export function useInProduction( + middlewares: RequestHandler[] +): RequestHandler[] { + return middlewares.map((middleware) => + isDevEnvironment() ? emptyMiddleware : middleware + ); +} diff --git a/backend/src/middlewares/validation.ts b/backend/src/middlewares/validation.ts new file mode 100644 index 000000000..106621b29 --- /dev/null +++ b/backend/src/middlewares/validation.ts @@ -0,0 +1,69 @@ +import _ from "lodash"; +import joi from "joi"; +import MonkeyError from "../utils/error"; +import { Response, NextFunction, RequestHandler } from "express"; + +type ValidationSchema = { + body?: object; + query?: object; + params?: object; + headers?: object; +}; + +type ValidationSchemaOption = { + allowUnknown?: boolean; +}; + +type ValidationHandlingOptions = { + validationErrorMessage?: string; +}; + +type ValidationSchemaOptions = { + [schema in keyof ValidationSchema]?: ValidationSchemaOption; +} & ValidationHandlingOptions; + +const VALIDATION_SCHEMA_DEFAULT_OPTIONS: ValidationSchemaOptions = { + body: { allowUnknown: false }, + headers: { allowUnknown: true }, + params: { allowUnknown: false }, + query: { allowUnknown: false }, +}; + +export function validateRequest( + validationSchema: ValidationSchema, + validationOptions: ValidationSchemaOptions = VALIDATION_SCHEMA_DEFAULT_OPTIONS +): RequestHandler { + const options = { + ...VALIDATION_SCHEMA_DEFAULT_OPTIONS, + ...validationOptions, + }; + const { validationErrorMessage } = options; + const normalizedValidationSchema: ValidationSchema = _.omit( + validationSchema, + "validationErrorMessage" + ); + + return (req: MonkeyTypes.Request, _res: Response, next: NextFunction) => { + _.each( + normalizedValidationSchema, + (schema: object, key: keyof ValidationSchema) => { + const joiSchema = joi + .object() + .keys(schema) + .unknown(options[key]?.allowUnknown); + + const { error } = joiSchema.validate(req[key] ?? {}); + if (error) { + const errorMessage = error.details[0]?.message; + throw new MonkeyError( + 422, + validationErrorMessage ?? + `${errorMessage} (${error.details[0]?.context?.value})` + ); + } + } + ); + + next(); + }; +}