This commit is contained in:
Miodec 2022-02-01 20:15:30 +01:00
commit 2dcfa38aa6
8 changed files with 173 additions and 73 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -584,5 +584,8 @@
["mobilize", "mobilise"],
["armor", "armour"],
["labeling", "labelling"],
["endeavor", "endeavour"]
["endeavor", "endeavour"],
["civilize", "civilise"],
["civilized", "civilised"],
["civilization", "civilisation"]
]

View file

@ -708,5 +708,10 @@
"name": "fleuriste",
"bgColor": "#c6b294",
"mainColor": "#405a52"
}
}
,{
"name": "dmg",
"bgColor": "#dadbdc",
"mainColor": "#3846b1"
}
]

33
static/themes/dmg.css Normal file
View file

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