Move quote moderation logic to quotes (#2590)

* Move quote moderation logic to quotes

* Add note

* Fix issue
This commit is contained in:
Bruce Berrios 2022-02-27 12:37:51 -05:00 committed by GitHub
parent 39b13804da
commit bbc8c837df
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
5 changed files with 127 additions and 71 deletions

View file

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

View file

@ -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<MonkeyResponse> {
const data = await NewQuotesDao.get();
return new MonkeyResponse("Quote submissions retrieved", data);
}
static async addQuote(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
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<MonkeyResponse> {
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<MonkeyResponse> {
const { quoteId } = req.body;
await NewQuotesDao.refuse(quoteId);
return new MonkeyResponse("Quote refused");
}
static async getRating(req: MonkeyTypes.Request): Promise<MonkeyResponse> {
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))) {

View file

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

View file

@ -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<T> {
criteria: (data: T) => boolean;
invalidMessage?: string;
}
@ -17,19 +18,53 @@ interface ConfigurationValidationOptions {
* the criteria.
*/
function validateConfiguration(
options: ConfigurationValidationOptions
options: ValidationOptions<MonkeyTypes.Configuration>
): 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<MonkeyTypes.User>
): 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,
};

View file

@ -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<Context>;
}
// 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<string, Record<string, number>>;
startedTests: number;
tags: object[];
timeTyping: number;
uid: string;
quoteMod: boolean;
cannotReport: boolean;
}
}