refactor: split middlewares into smaller files (#5616)

* split

* fix imports

* rename
This commit is contained in:
Jack 2024-07-15 17:08:32 +02:00 committed by GitHub
parent 088ff638cc
commit 2af5879f23
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
19 changed files with 261 additions and 274 deletions

View file

@ -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;
},

View file

@ -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;
},

View file

@ -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();

View file

@ -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();

View file

@ -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();
},

View file

@ -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 {

View file

@ -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;
},

View file

@ -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();

View file

@ -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();

View file

@ -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

View file

@ -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;
},

View file

@ -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;
},

View file

@ -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;
},

View file

@ -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";

View file

@ -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<T> = {
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<SharedTypes.Configuration>
): 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<MonkeyTypes.DBUser>
): 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<MonkeyResponse>;
/**
* 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,
};

View file

@ -0,0 +1,31 @@
import { Response, NextFunction, RequestHandler } from "express";
import MonkeyError from "../utils/error";
export type ValidationOptions<T> = {
criteria: (data: T) => boolean;
invalidMessage?: string;
};
/**
* This utility checks that the server's configuration matches
* the criteria.
*/
export function validate(
options: ValidationOptions<SharedTypes.Configuration>
): 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();
};
}

View file

@ -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<MonkeyTypes.DBUser>
): 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();
};
}

View file

@ -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<MonkeyResponse>;
/**
* 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
);
}

View file

@ -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();
};
}