From aefb60a2dc2666432b8b9f0bc7b51c88932f8aff Mon Sep 17 00:00:00 2001 From: M Usman Abubakr <86719130+Usman-Abubakr@users.noreply.github.com> Date: Tue, 1 Feb 2022 18:09:56 +0000 Subject: [PATCH 1/3] Added DMG theme (#2392) by Usman-Abubakr --- static/themes/_list.json | 7 ++++++- static/themes/dmg.css | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+), 1 deletion(-) create mode 100644 static/themes/dmg.css diff --git a/static/themes/_list.json b/static/themes/_list.json index c40fce476..e28bf5937 100644 --- a/static/themes/_list.json +++ b/static/themes/_list.json @@ -708,5 +708,10 @@ "name": "fleuriste", "bgColor": "#c6b294", "mainColor": "#405a52" - } + } + ,{ + "name": "dmg", + "bgColor": "#dadbdc", + "mainColor": "#3846b1" + } ] diff --git a/static/themes/dmg.css b/static/themes/dmg.css new file mode 100644 index 000000000..e4a2bd09b --- /dev/null +++ b/static/themes/dmg.css @@ -0,0 +1,33 @@ +:root { + --bg-color: #dadbdc; + --main-color: #ae185e; + --caret-color: #384693; + --sub-color: #3846b1; + --text-color: #414141; + --error-color: #ae185e; + --error-extra-color: #93335c; + --colorful-error-color: #80a053; + --colorful-error-extra-color: #306230; +} + +#menu { + gap: 0.5rem; +} + +#top.focus #menu .icon-button { + background: var(--bg-color); + size: 1rem; +} +#top.focus #menu .icon-button:nth-child(1) { + background: #e34c6c; +} +#top.focus #menu:before, +#top.focus #menu:after { + background: var(--sub-color); +} + +#menu .icon-button { + border-radius: 10rem !important; + color: var(--bg-color); + background: var(--main-color); +} From 89ca9e58301a736753b7257cd0370c9e8c5b52c8 Mon Sep 17 00:00:00 2001 From: Theo Pearson-Bray Date: Tue, 1 Feb 2022 18:11:36 +0000 Subject: [PATCH 2/3] Add "civilise" to British English (#2391) by tpb1908 * Add "civilise" and "civilisation" to British English "civilize" is in 4 quotes "civilization" is in 24 quotes * Add "civilized" to British English Same root as "civilize" --- static/languages/britishenglish.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/static/languages/britishenglish.json b/static/languages/britishenglish.json index 6475e6951..a5a347edb 100644 --- a/static/languages/britishenglish.json +++ b/static/languages/britishenglish.json @@ -584,5 +584,8 @@ ["mobilize", "mobilise"], ["armor", "armour"], ["labeling", "labelling"], - ["endeavor", "endeavour"] + ["endeavor", "endeavour"], + ["civilize", "civilise"], + ["civilized", "civilised"], + ["civilization", "civilisation"] ] From 92a503df71c4301105a8b5cf462374e32b1ed17b Mon Sep 17 00:00:00 2001 From: Bruce Berrios <58147810+Bruception@users.noreply.github.com> Date: Tue, 1 Feb 2022 13:47:41 -0500 Subject: [PATCH 3/3] Refactor Quote Endpoints (#2388) by Bruception * Refactor quotes route * Add request validation * removed unnecessary configuration check * using const Co-authored-by: Miodec --- backend/api/controllers/new-quotes.js | 41 +++++--------- backend/api/controllers/quote-ratings.js | 35 +++--------- backend/api/routes/quotes.js | 49 ++++++++++++++++ backend/middlewares/api-utils.js | 72 ++++++++++++++++++------ src/js/popups/quote-rate-popup.js | 4 +- 5 files changed, 130 insertions(+), 71 deletions(-) diff --git a/backend/api/controllers/new-quotes.js b/backend/api/controllers/new-quotes.js index da9568967..98a2ad38b 100644 --- a/backend/api/controllers/new-quotes.js +++ b/backend/api/controllers/new-quotes.js @@ -5,52 +5,41 @@ const Logger = require("../../handlers/logger.js"); const Captcha = require("../../handlers/captcha"); class NewQuotesController { - static async getQuotes(req, res) { + static async getQuotes(req, _res) { const { uid } = req.decodedToken; const userInfo = await UserDAO.getUser(uid); if (!userInfo.quoteMod) { throw new MonkeyError(403, "You don't have permission to do this"); } - let data = await NewQuotesDAO.get(); - return res.status(200).json(data); + return await NewQuotesDAO.get(); } - static async addQuote(req, res) { - if (req.context.configuration.quoteSubmit.enabled === false) - throw new MonkeyError( - 500, - "Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up." - ); - let { uid } = req.decodedToken; - let { text, source, language, captcha } = req.body; - if (!text || !source || !language) { - throw new MonkeyError(400, "Please fill all the fields"); - } + static async addQuote(req, _res) { + const { uid } = req.decodedToken; + const { text, source, language, captcha } = req.body; if (!(await Captcha.verify(captcha))) { throw new MonkeyError(400, "Captcha check failed"); } - let data = await NewQuotesDAO.add(text, source, language, uid); - return res.status(200).json(data); + return await NewQuotesDAO.add(text, source, language, uid); } - static async approve(req, res) { - let { uid } = req.decodedToken; - let { quoteId, editText, editSource } = req.body; + static async approve(req, _res) { + const { uid } = req.decodedToken; + const { quoteId, editText, editSource } = req.body; const userInfo = await UserDAO.getUser(uid); if (!userInfo.quoteMod) { throw new MonkeyError(403, "You don't have permission to do this"); } - if (editText === "" || editSource === "") { - throw new MonkeyError(400, "Please fill all the fields"); - } - let data = await NewQuotesDAO.approve(quoteId, editText, editSource); + const data = await NewQuotesDAO.approve(quoteId, editText, editSource); Logger.log("system_quote_approved", data, uid); - return res.status(200).json(data); + + return data; } static async refuse(req, res) { - let { uid } = req.decodedToken; - let { quoteId } = req.body; + const { uid } = req.decodedToken; + const { quoteId } = req.body; + await NewQuotesDAO.refuse(quoteId, uid); return res.sendStatus(200); } diff --git a/backend/api/controllers/quote-ratings.js b/backend/api/controllers/quote-ratings.js index ad40fb51d..4dbca0525 100644 --- a/backend/api/controllers/quote-ratings.js +++ b/backend/api/controllers/quote-ratings.js @@ -3,49 +3,32 @@ const UserDAO = require("../../dao/user"); const MonkeyError = require("../../handlers/error"); class QuoteRatingsController { - static async getRating(req, res) { + static async getRating(req, _res) { const { quoteId, language } = req.query; - let data = await QuoteRatingsDAO.get(parseInt(quoteId), language); - return res.status(200).json(data); + return await QuoteRatingsDAO.get(parseInt(quoteId), language); } + static async submitRating(req, res) { - let { uid } = req.decodedToken; + const { uid } = req.decodedToken; let { quoteId, rating, language } = req.body; + quoteId = parseInt(quoteId); - rating = parseInt(rating); - if (isNaN(quoteId) || isNaN(rating)) { - throw new MonkeyError( - 400, - "Bad request. Quote id or rating is not a number." - ); - } - if (typeof language !== "string") { - throw new MonkeyError(400, "Bad request. Language is not a string."); - } - - if (rating < 1 || rating > 5) { - throw new MonkeyError( - 400, - "Bad request. Rating must be between 1 and 5." - ); - } - - rating = Math.round(rating); + rating = Math.round(parseInt(rating)); //check if user already submitted a rating - let user = await UserDAO.getUser(uid); + const user = await UserDAO.getUser(uid); if (!user) { throw new MonkeyError(401, "User not found."); } - let quoteRatings = user.quoteRatings; + const quoteRatings = user.quoteRatings; if (quoteRatings === undefined) quoteRatings = {}; if (quoteRatings[language] === undefined) quoteRatings[language] = {}; if (quoteRatings[language][quoteId] == undefined) quoteRatings[language][quoteId] = undefined; - let quoteRating = quoteRatings[language][quoteId]; + const quoteRating = quoteRatings[language][quoteId]; let newRating; let update; diff --git a/backend/api/routes/quotes.js b/backend/api/routes/quotes.js index 2aa28e794..6d66aded4 100644 --- a/backend/api/routes/quotes.js +++ b/backend/api/routes/quotes.js @@ -8,6 +8,7 @@ const RateLimit = require("../../middlewares/rate-limit"); const { asyncHandlerWrapper, requestValidation, + validateConfiguration, } = require("../../middlewares/api-utils"); const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quote-languages"); @@ -22,8 +23,24 @@ quotesRouter.get( quotesRouter.post( "/", + validateConfiguration({ + criteria: (configuration) => { + return configuration.quoteSubmit.enabled; + }, + invalidMessage: + "Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.", + }), RateLimit.newQuotesAdd, authenticateRequest, + requestValidation({ + body: { + text: joi.string().min(60).required(), + source: joi.string().required(), + language: joi.string().required(), + captcha: joi.string().required(), + }, + validationErrorMessage: "Please fill all the fields", + }), asyncHandlerWrapper(NewQuotesController.addQuote) ); @@ -31,6 +48,14 @@ quotesRouter.post( "/approve", RateLimit.newQuotesAction, authenticateRequest, + requestValidation({ + body: { + quoteId: joi.string().required(), + editText: joi.string().required(), + editSource: joi.string().required(), + }, + validationErrorMessage: "Please fill all the fields", + }), asyncHandlerWrapper(NewQuotesController.approve) ); @@ -38,6 +63,11 @@ quotesRouter.post( "/reject", RateLimit.newQuotesAction, authenticateRequest, + requestValidation({ + body: { + quoteId: joi.string().required(), + }, + }), asyncHandlerWrapper(NewQuotesController.refuse) ); @@ -45,6 +75,12 @@ quotesRouter.get( "/rating", RateLimit.quoteRatingsGet, authenticateRequest, + requestValidation({ + query: { + quoteId: joi.string().regex(/^\d+$/).required(), + language: joi.string().required(), + }, + }), asyncHandlerWrapper(QuoteRatingsController.getRating) ); @@ -52,11 +88,24 @@ quotesRouter.post( "/rating", RateLimit.quoteRatingsSubmit, authenticateRequest, + requestValidation({ + body: { + quoteId: joi.number().required(), + rating: joi.number().min(1).max(5).required(), + language: joi.string().required(), + }, + }), asyncHandlerWrapper(QuoteRatingsController.submitRating) ); quotesRouter.post( "/report", + validateConfiguration({ + criteria: (configuration) => { + return configuration.quoteReport.enabled; + }, + invalidMessage: "Quote reporting is unavailable.", + }), RateLimit.quoteReportSubmit, authenticateRequest, requestValidation({ diff --git a/backend/middlewares/api-utils.js b/backend/middlewares/api-utils.js index c4f0308e1..8fe62f185 100644 --- a/backend/middlewares/api-utils.js +++ b/backend/middlewares/api-utils.js @@ -1,6 +1,29 @@ +const _ = require("lodash"); const joi = require("joi"); const MonkeyError = require("../handlers/error"); +/** + * This utility checks that the server's configuration matches + * the criteria. + */ +function validateConfiguration(options) { + const { criteria, invalidMessage } = options; + + return (req, res, next) => { + const configuration = req.context.configuration; + + const validated = criteria(configuration); + if (!validated) { + throw new MonkeyError( + 503, + invalidMessage ?? "This service is currently unavailable." + ); + } + + next(); + }; +} + /** * 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. @@ -11,8 +34,13 @@ function asyncHandlerWrapper(handler) { return async (req, res, next) => { try { const handlerData = await handler(req, res); - if (!res.headersSent && handlerData) { - res.json(handlerData); + + if (!res.headersSent) { + if (handlerData) { + res.json(handlerData); + } else { + res.sendStatus(204); + } } next(); } catch (error) { @@ -22,26 +50,35 @@ function asyncHandlerWrapper(handler) { } function requestValidation(validationSchema) { - return (req, res, next) => { - /** - * In dev environments, as an alternative to token authentication, - * you can pass the authentication middleware by having a user id in the body. - * Inject the user id into the schema so that validation will not fail. - */ - if (process.env.MODE === "dev") { - validationSchema.body = { - uid: joi.any(), - ...(validationSchema.body ?? {}), - }; - } + /** + * In dev environments, as an alternative to token authentication, + * you can pass the authentication middleware by having a user id in the body. + * Inject the user id into the schema so that validation will not fail. + */ + if (process.env.MODE === "dev") { + validationSchema.body = { + uid: joi.any(), + ...(validationSchema.body ?? {}), + }; + } - Object.keys(validationSchema).forEach((key) => { - const schema = validationSchema[key]; + const { validationErrorMessage } = validationSchema; + const normalizedValidationSchema = _.omit( + validationSchema, + "validationErrorMessage" + ); + + return (req, res, next) => { + _.each(normalizedValidationSchema, (schema, key) => { const joiSchema = joi.object().keys(schema); + const { error } = joiSchema.validate(req[key] ?? {}); if (error) { const errorMessage = error.details[0].message; - throw new MonkeyError(400, `Invalid request: ${errorMessage}`); + throw new MonkeyError( + 400, + validationErrorMessage ?? `Invalid request: ${errorMessage}` + ); } }); @@ -50,6 +87,7 @@ function requestValidation(validationSchema) { } module.exports = { + validateConfiguration, asyncHandlerWrapper, requestValidation, }; diff --git a/src/js/popups/quote-rate-popup.js b/src/js/popups/quote-rate-popup.js index 372acdba5..5ff43d273 100644 --- a/src/js/popups/quote-rate-popup.js +++ b/src/js/popups/quote-rate-popup.js @@ -32,10 +32,10 @@ export async function getQuoteStats(quote) { return; } Loader.hide(); - if (response.status !== 200) { + if (response.status !== 200 && response.status !== 204) { Notifications.add(response.data.message); } else { - if (response.data === null) { + if (response.status === 204) { quoteStats = {}; } else { quoteStats = response.data;