diff --git a/backend/api/controllers/new-quotes.js b/backend/api/controllers/new-quotes.js deleted file mode 100644 index 89a29a56b..000000000 --- a/backend/api/controllers/new-quotes.js +++ /dev/null @@ -1,50 +0,0 @@ -import NewQuotesDao from "../../dao/new-quotes"; -import MonkeyError from "../../handlers/error"; -import UsersDAO from "../../dao/user"; -import Logger from "../../handlers/logger.js"; -import { verify } from "../../handlers/captcha"; -import { MonkeyResponse } from "../../handlers/monkey-response"; - -class NewQuotesController { - static async getQuotes(req, _res) { - const { uid } = req.ctx.decodedToken; - const userInfo = await UsersDAO.getUser(uid); - if (!userInfo.quoteMod) { - throw new MonkeyError(403, "You don't have permission to do this"); - } - const data = await NewQuotesDao.get(); - return new MonkeyResponse("Quote submissions retrieved", data); - } - - static async addQuote(req, _res) { - const { uid } = req.ctx.decodedToken; - const { text, source, language, captcha } = req.body; - if (!(await verify(captcha))) { - throw new MonkeyError(400, "Captcha check failed"); - } - await NewQuotesDao.add(text, source, language, uid); - return new MonkeyResponse("Quote submission added"); - } - - static async approve(req, _res) { - const { uid } = req.ctx.decodedToken; - const { quoteId, editText, editSource } = req.body; - const userInfo = await UsersDAO.getUser(uid); - if (!userInfo.quoteMod) { - throw new MonkeyError(403, "You don't have permission to do this"); - } - const data = await NewQuotesDao.approve(quoteId, editText, editSource); - Logger.log("system_quote_approved", data, uid); - - return new MonkeyResponse(data.message, data.quote); - } - - static async refuse(req, _res) { - const { quoteId } = req.body; - - await NewQuotesDao.refuse(quoteId); - return new MonkeyResponse("Quote refused"); - } -} - -export default NewQuotesController; diff --git a/backend/api/controllers/quotes.ts b/backend/api/controllers/quotes.ts index 82f02dccb..5b2b656a3 100644 --- a/backend/api/controllers/quotes.ts +++ b/backend/api/controllers/quotes.ts @@ -2,14 +2,48 @@ import _ from "lodash"; import { v4 as uuidv4 } from "uuid"; import UserDAO from "../../dao/user"; import ReportDAO from "../../dao/report"; +import NewQuotesDao from "../../dao/new-quotes"; import QuoteRatingsDAO from "../../dao/quote-ratings"; -import UsersDAO from "../../dao/user"; import MonkeyError from "../../handlers/error"; import { verify } from "../../handlers/captcha"; import Logger from "../../handlers/logger"; import { MonkeyResponse } from "../../handlers/monkey-response"; class QuotesController { + static async getQuotes(_req: MonkeyTypes.Request): Promise { + const data = await NewQuotesDao.get(); + return new MonkeyResponse("Quote submissions retrieved", data); + } + + static async addQuote(req: MonkeyTypes.Request): Promise { + const { uid } = req.ctx.decodedToken; + const { text, source, language, captcha } = req.body; + + if (!(await verify(captcha))) { + throw new MonkeyError(400, "Captcha check failed"); + } + + await NewQuotesDao.add(text, source, language, uid); + return new MonkeyResponse("Quote submission added"); + } + + static async approveQuote(req: MonkeyTypes.Request): Promise { + const { uid } = req.ctx.decodedToken; + const { quoteId, editText, editSource } = req.body; + + const data = await NewQuotesDao.approve(quoteId, editText, editSource); + Logger.log("system_quote_approved", data, uid); + + return new MonkeyResponse(data.message, data.quote); + } + + static async refuseQuote(req: MonkeyTypes.Request): Promise { + const { quoteId } = req.body; + + await NewQuotesDao.refuse(quoteId); + return new MonkeyResponse("Quote refused"); + } + static async getRating(req: MonkeyTypes.Request): Promise { const { quoteId, language } = req.query; @@ -67,11 +101,6 @@ class QuotesController { quoteReport: { maxReports, contentReportLimit }, } = req.ctx.configuration; - const user = await UsersDAO.getUser(uid); - if (user.cannotReport) { - throw new MonkeyError(403, "You don't have permission to do this."); - } - const { quoteId, quoteLanguage, reason, comment, captcha } = req.body; if (!(await verify(captcha))) { diff --git a/backend/api/routes/quotes.ts b/backend/api/routes/quotes.ts index c2d23c915..89ee10bd8 100644 --- a/backend/api/routes/quotes.ts +++ b/backend/api/routes/quotes.ts @@ -1,11 +1,11 @@ import joi from "joi"; import { authenticateRequest } from "../../middlewares/auth"; import { Router } from "express"; -import NewQuotesController from "../controllers/new-quotes"; import QuotesController from "../controllers/quotes"; import * as RateLimit from "../../middlewares/rate-limit"; import { asyncHandler, + checkUserPermissions, validateConfiguration, validateRequest, } from "../../middlewares/api-utils"; @@ -13,11 +13,18 @@ import SUPPORTED_QUOTE_LANGUAGES from "../../constants/quote-languages"; const quotesRouter = Router(); +const checkIfUserIsQuoteMod = checkUserPermissions({ + criteria: (user) => { + return user.quoteMod; + }, +}); + quotesRouter.get( "/", RateLimit.newQuotesGet, authenticateRequest(), - asyncHandler(NewQuotesController.getQuotes) + checkIfUserIsQuoteMod, + asyncHandler(QuotesController.getQuotes) ); quotesRouter.post( @@ -40,7 +47,7 @@ quotesRouter.post( }, validationErrorMessage: "Please fill all the fields", }), - asyncHandler(NewQuotesController.addQuote) + asyncHandler(QuotesController.addQuote) ); quotesRouter.post( @@ -55,7 +62,8 @@ quotesRouter.post( }, validationErrorMessage: "Please fill all the fields", }), - asyncHandler(NewQuotesController.approve) + checkIfUserIsQuoteMod, + asyncHandler(QuotesController.approveQuote) ); quotesRouter.post( @@ -67,7 +75,8 @@ quotesRouter.post( quoteId: joi.string().required(), }, }), - asyncHandler(NewQuotesController.refuse) + checkIfUserIsQuoteMod, + asyncHandler(QuotesController.refuseQuote) ); quotesRouter.get( @@ -127,6 +136,11 @@ quotesRouter.post( captcha: joi.string().required(), }, }), + checkUserPermissions({ + criteria: (user) => { + return !user.cannotReport; + }, + }), asyncHandler(QuotesController.reportQuote) ); diff --git a/backend/middlewares/api-utils.ts b/backend/middlewares/api-utils.ts index a729afbac..72361ad52 100644 --- a/backend/middlewares/api-utils.ts +++ b/backend/middlewares/api-utils.ts @@ -6,9 +6,10 @@ import { handleMonkeyResponse, MonkeyResponse, } from "../handlers/monkey-response"; +import UsersDAO from "../dao/user"; -interface ConfigurationValidationOptions { - criteria: (configuration: MonkeyTypes.Configuration) => boolean; +interface ValidationOptions { + criteria: (data: T) => boolean; invalidMessage?: string; } @@ -17,19 +18,53 @@ interface ConfigurationValidationOptions { * the criteria. */ function validateConfiguration( - options: ConfigurationValidationOptions + options: ValidationOptions ): RequestHandler { - const { criteria, invalidMessage } = options; + 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 ?? "This service is currently unavailable." - ); + throw new MonkeyError(503, invalidMessage); + } + + 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 UsersDAO.getUser( + uid + )) as unknown as MonkeyTypes.User; + const hasPermission = criteria(userData); + + if (!hasPermission) { + throw new MonkeyError(403, invalidMessage); + } + } catch (error) { + next(error); } next(); @@ -110,4 +145,9 @@ function validateRequest(validationSchema: ValidationSchema): RequestHandler { }; } -export { validateConfiguration, asyncHandler, validateRequest }; +export { + validateConfiguration, + checkUserPermissions, + asyncHandler, + validateRequest, +}; diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts index 38062d811..a198d6f48 100644 --- a/backend/types/types.d.ts +++ b/backend/types/types.d.ts @@ -21,7 +21,6 @@ declare namespace MonkeyTypes { enabled: boolean; }; } - interface DecodedToken { uid?: string; email?: string; @@ -35,4 +34,28 @@ declare namespace MonkeyTypes { interface Request extends ExpressRequest { ctx: Readonly; } + + // Data Model + + interface User { + // TODO, Complete the typings for the user model + _id: string; + addedAt: number; + bananas: number; + completedTests: number; + discordId?: string; + email: string; + lastNameChange: number; + lbMemory: object; + lbPersonalBests: object; + name: string; + personalBests: object; + quoteRatings: Record>; + startedTests: number; + tags: object[]; + timeTyping: number; + uid: string; + quoteMod: boolean; + cannotReport: boolean; + } }