mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-10-16 18:46:26 +08:00
refactor: split middlewares into smaller files (#5616)
* split * fix imports * rename
This commit is contained in:
parent
088ff638cc
commit
2af5879f23
19 changed files with 261 additions and 274 deletions
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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();
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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;
|
||||
},
|
||||
|
|
|
@ -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";
|
||||
|
||||
|
|
|
@ -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,
|
||||
};
|
31
backend/src/middlewares/configuration.ts
Normal file
31
backend/src/middlewares/configuration.ts
Normal 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();
|
||||
};
|
||||
}
|
63
backend/src/middlewares/permission.ts
Normal file
63
backend/src/middlewares/permission.ts
Normal 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();
|
||||
};
|
||||
}
|
47
backend/src/middlewares/utility.ts
Normal file
47
backend/src/middlewares/utility.ts
Normal 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
|
||||
);
|
||||
}
|
69
backend/src/middlewares/validation.ts
Normal file
69
backend/src/middlewares/validation.ts
Normal 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();
|
||||
};
|
||||
}
|
Loading…
Add table
Reference in a new issue