mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-02-03 20:40:48 +08:00
Merge branch 'master' of https://github.com/Miodec/monkeytype
This commit is contained in:
commit
2dcfa38aa6
8 changed files with 173 additions and 73 deletions
|
@ -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);
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -584,5 +584,8 @@
|
|||
["mobilize", "mobilise"],
|
||||
["armor", "armour"],
|
||||
["labeling", "labelling"],
|
||||
["endeavor", "endeavour"]
|
||||
["endeavor", "endeavour"],
|
||||
["civilize", "civilise"],
|
||||
["civilized", "civilised"],
|
||||
["civilization", "civilisation"]
|
||||
]
|
||||
|
|
|
@ -708,5 +708,10 @@
|
|||
"name": "fleuriste",
|
||||
"bgColor": "#c6b294",
|
||||
"mainColor": "#405a52"
|
||||
}
|
||||
}
|
||||
,{
|
||||
"name": "dmg",
|
||||
"bgColor": "#dadbdc",
|
||||
"mainColor": "#3846b1"
|
||||
}
|
||||
]
|
||||
|
|
33
static/themes/dmg.css
Normal file
33
static/themes/dmg.css
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue