Api overhaul (#2555) by Bruception

* Feat:Update response structure (#2427)

* Fix:response and error structure

* update:response message

* update:response class

* update

* Update response message

Co-authored-by: Mustafiz Kaifee Mumtaz <mustafiz.mumtaz@freecharge.com>

* Add MonkeyToken foundation (#2487) by Bruception

* Api changes (#2492)

* API changes

* Remove unused import

* Add Ape client (#2513)

* Add all endpoints (#2514)

* Merged backend typescript into api overhaul (#2515)

* Install typescript and add backend tsconfig

Cannot yet build due to a number of compilation errors in JS code

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Fix typescript compilation errors

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Migrated backend to ES modules

Switched to import export syntax

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Add typescript declaration for anticheat

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Rename top level files to .ts

Fix service account json file typing

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Add dev build scripts for backend typescript

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Removed empty lines and switched to using db

Cleaned up imports by removing needless empty lines and migrated to the new db.js instead of mongodb.js.

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Fixed backend commonjs syntax to ES module syntax

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Add build to backend start script

Signed-off-by: Brian Evans <ebrian101@gmail.com>

* Migrate some endpoints to Ape

* Strict equals

* Remove artifact

* ape -> Ape

* Ape migration p2 (#2522)

* Migrate leaderboard endpoints to ape

* Fixed comment

* Init backend types

* Fail

* Return

* Migrate Quotes to Ape (#2528)

* Migrate quotes to Ape

* Fix backend response

* Fix issue

* Fix rate limit (#2533)

* fix rate limit

* Fix import

* Fix issues

* Ape migration p4 (#2547)

* Migrate results endpoints to ape

* Remove unused import

* Remove unused import

* Fix loaders

* Make function async

* Hide try saving results

* Migrate some users endpoints to Ape (#2548)

* Complete Ape Migration (#2553)

* Complete ape migration

* Fix preset

* Return preset data

* Add typings

* Move captcha reset

* Read from params

* Fix result tags endpoint

* Fix stuck loader

* fixed lb memory not saving

* fixed quote rating popup not showing up for new users

Co-authored-by: Mustafiz Kaifee <49086821+Mustafiz04@users.noreply.github.com>
Co-authored-by: Mustafiz Kaifee Mumtaz <mustafiz.mumtaz@freecharge.com>
Co-authored-by: Brian Evans <53117772+mrbrianevans@users.noreply.github.com>
Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
Bruce Berrios 2022-02-22 14:55:48 -05:00 committed by GitHub
parent 9eb4e6ac86
commit f9d6f52c15
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
98 changed files with 2777 additions and 2417 deletions

2
.gitignore vendored
View file

@ -102,4 +102,4 @@ backend/anticheat
dep-graph.png
# TypeScript
built/
build/

View file

@ -7,3 +7,4 @@ sound/*
node_modules
css/balloon.css
_list.json
backend/build

1
backend/.gitignore vendored
View file

@ -1,3 +1,4 @@
lastId.txt
log_success.txt
log_failed.txt
build

View file

@ -1,21 +1,24 @@
const ConfigDAO = require("../../dao/config");
const { validateConfig } = require("../../handlers/validation");
import ConfigDAO from "../../dao/config";
import { validateConfig } from "../../handlers/validation";
import { MonkeyResponse } from "../../handlers/monkey-response";
class ConfigController {
static async getConfig(req, _res) {
const { uid } = req.ctx.decodedToken;
return await ConfigDAO.getConfig(uid);
const data = await ConfigDAO.getConfig(uid);
return new MonkeyResponse("Configuration retrieved", data);
}
static async saveConfig(req, res) {
static async saveConfig(req, _res) {
const { config } = req.body;
const { uid } = req.ctx.decodedToken;
validateConfig(config);
await ConfigDAO.saveConfig(uid, config);
return res.sendStatus(200);
return new MonkeyResponse("Config updated");
}
}
module.exports = ConfigController;
export default ConfigController;

View file

@ -1,5 +1,6 @@
const _ = require("lodash");
const LeaderboardsDAO = require("../../dao/leaderboards");
import { MonkeyResponse } from "../../handlers/monkey-response";
import _ from "lodash";
import LeaderboardsDAO from "../../dao/leaderboards";
class LeaderboardsController {
static async get(req, _res) {
@ -20,21 +21,15 @@ class LeaderboardsController {
: _.omit(entry, ["discordId", "uid", "difficulty", "language"]);
});
return normalizedLeaderboard;
return new MonkeyResponse("Leaderboard retrieved", normalizedLeaderboard);
}
static async getRank(req, res) {
static async getRank(req, _res) {
const { language, mode, mode2 } = req.query;
const { uid } = req.ctx.decodedToken;
if (!uid) {
return res.status(400).json({
message: "Missing user id.",
});
}
return await LeaderboardsDAO.getRank(mode, mode2, language, uid);
const data = await LeaderboardsDAO.getRank(mode, mode2, language, uid);
return new MonkeyResponse("Rank retrieved", data);
}
}
module.exports = LeaderboardsController;
export default LeaderboardsController;

View file

@ -1,48 +1,50 @@
const NewQuotesDAO = require("../../dao/new-quotes");
const MonkeyError = require("../../handlers/error");
const UserDAO = require("../../dao/user");
const Logger = require("../../handlers/logger.js");
const Captcha = require("../../handlers/captcha");
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 UserDAO.getUser(uid);
const userInfo = await UsersDAO.getUser(uid);
if (!userInfo.quoteMod) {
throw new MonkeyError(403, "You don't have permission to do this");
}
return await NewQuotesDAO.get();
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 Captcha.verify(captcha))) {
if (!(await verify(captcha))) {
throw new MonkeyError(400, "Captcha check failed");
}
return await NewQuotesDAO.add(text, source, language, uid);
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 UserDAO.getUser(uid);
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);
const data = await NewQuotesDao.approve(quoteId, editText, editSource);
Logger.log("system_quote_approved", data, uid);
return data;
return new MonkeyResponse(data.message, data.quote);
}
static async refuse(req, res) {
const { uid } = req.ctx.decodedToken;
static async refuse(req, _res) {
const { quoteId } = req.body;
await NewQuotesDAO.refuse(quoteId, uid);
return res.sendStatus(200);
await NewQuotesDao.refuse(quoteId);
return new MonkeyResponse("Quote refused");
}
}
module.exports = NewQuotesController;
export default NewQuotesController;

View file

@ -1,36 +1,40 @@
const PresetDAO = require("../../dao/preset");
import PresetDAO from "../../dao/preset";
import { MonkeyResponse } from "../../handlers/monkey-response";
class PresetController {
static async getPresets(req, _res) {
const { uid } = req.ctx.decodedToken;
return await PresetDAO.getPresets(uid);
const data = await PresetDAO.getPresets(uid);
return new MonkeyResponse("Preset retrieved", data);
}
static async addPreset(req, _res) {
const { name, config } = req.body;
const { uid } = req.ctx.decodedToken;
return await PresetDAO.addPreset(uid, name, config);
const data = await PresetDAO.addPreset(uid, name, config);
return new MonkeyResponse("Preset created", data);
}
static async editPreset(req, res) {
static async editPreset(req, _res) {
const { _id, name, config } = req.body;
const { uid } = req.ctx.decodedToken;
await PresetDAO.editPreset(uid, _id, name, config);
return res.sendStatus(200);
return new MonkeyResponse("Preset updated");
}
static async removePreset(req, res) {
const { _id } = req.body;
static async removePreset(req, _res) {
const { presetId } = req.params;
const { uid } = req.ctx.decodedToken;
await PresetDAO.removePreset(uid, _id);
await PresetDAO.removePreset(uid, presetId);
return res.sendStatus(200);
return new MonkeyResponse("Preset deleted");
}
}
module.exports = PresetController;
export default PresetController;

View file

@ -1,10 +1,11 @@
const PsaDAO = require("../../dao/psa");
import PsaDAO from "../../dao/psa";
import { MonkeyResponse } from "../../handlers/monkey-response";
class PsaController {
static async get(req, res) {
static async get(_req, _res) {
let data = await PsaDAO.get();
return res.status(200).json(data);
return new MonkeyResponse("PSAs retrieved", data);
}
}
module.exports = PsaController;
export default PsaController;

View file

@ -1,14 +1,16 @@
const QuoteRatingsDAO = require("../../dao/quote-ratings");
const UserDAO = require("../../dao/user");
const MonkeyError = require("../../handlers/error");
import MonkeyError from "../../handlers/error";
import UserDAO from "../../dao/user";
import QuoteRatingsDAO from "../../dao/quote-ratings";
import { MonkeyResponse } from "../../handlers/monkey-response";
class QuoteRatingsController {
static async getRating(req, _res) {
const { quoteId, language } = req.query;
return await QuoteRatingsDAO.get(parseInt(quoteId), language);
const data = await QuoteRatingsDAO.get(parseInt(quoteId), language);
return new MonkeyResponse("Rating retrieved", data);
}
static async submitRating(req, res) {
static async submitRating(req, _res) {
const { uid } = req.ctx.decodedToken;
let { quoteId, rating, language } = req.body;
@ -45,9 +47,8 @@ class QuoteRatingsController {
await QuoteRatingsDAO.submit(quoteId, language, newRating, update);
quoteRatings[language][quoteId] = rating;
await UserDAO.updateQuoteRatings(uid, quoteRatings);
return res.sendStatus(200);
return new MonkeyResponse("Rating updated");
}
}
module.exports = QuoteRatingsController;
export default QuoteRatingsController;

View file

@ -1,22 +1,23 @@
const { v4: uuidv4 } = require("uuid");
const ReportDAO = require("../../dao/report");
const UserDAO = require("../../dao/user");
const MonkeyError = require("../../handlers/error");
const Captcha = require("../../handlers/captcha");
const Logger = require("../../handlers/logger");
import { v4 as uuidv4 } from "uuid";
import ReportDAO from "../../dao/report";
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 reportQuote(req, res) {
static async reportQuote(req, _res) {
const { uid } = req.ctx.decodedToken;
const user = await UserDAO.getUser(uid);
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 Captcha.verify(captcha))) {
if (!(await verify(captcha))) {
throw new MonkeyError(400, "Captcha check failed.");
}
@ -39,8 +40,8 @@ class QuotesController {
details: newReport.details,
});
res.sendStatus(200);
return new MonkeyResponse("Quote reported successfully");
}
}
module.exports = QuotesController;
export default QuotesController;

View file

@ -1,20 +1,22 @@
const ResultDAO = require("../../dao/result");
const UserDAO = require("../../dao/user");
const PublicStatsDAO = require("../../dao/public-stats");
const BotDAO = require("../../dao/bot");
const { validateObjectValues } = require("../../handlers/validation");
const { stdDev, roundTo2 } = require("../../handlers/misc");
const objecthash = require("node-object-hash")().hash;
const Logger = require("../../handlers/logger");
const path = require("path");
const { config } = require("dotenv");
config({ path: path.join(__dirname, ".env") });
import ResultDAO from "../../dao/result";
import UserDAO from "../../dao/user";
import PublicStatsDAO from "../../dao/public-stats";
import BotDAO from "../../dao/bot";
import { validateObjectValues } from "../../handlers/validation";
import { roundTo2, stdDev } from "../../handlers/misc";
import node_object_hash from "node-object-hash";
import Logger from "../../handlers/logger";
import "dotenv/config";
import { MonkeyResponse } from "../../handlers/monkey-response";
import MonkeyError from "../../handlers/error";
const objecthash = node_object_hash().hash;
let validateResult;
let validateKeys;
try {
// eslint-disable-next-line
let module = require("../../anticheat/anticheat");
let module = require("anticheat");
validateResult = module.validateResult;
validateKeys = module.validateKeys;
if (!validateResult || !validateKeys) throw new Error("undefined");
@ -35,36 +37,34 @@ try {
class ResultController {
static async getResults(req, _res) {
const { uid } = req.ctx.decodedToken;
return await ResultDAO.getResults(uid);
const results = await ResultDAO.getResults(uid);
return new MonkeyResponse("Result retrieved", results);
}
static async deleteAll(req, res) {
static async deleteAll(req, _res) {
const { uid } = req.ctx.decodedToken;
await ResultDAO.deleteAll(uid);
Logger.log("user_results_deleted", "", uid);
return res.sendStatus(200);
return new MonkeyResponse("All results deleted");
}
static async updateTags(req, res) {
static async updateTags(req, _res) {
const { uid } = req.ctx.decodedToken;
const { tags, resultid } = req.body;
const { tagIds, resultId } = req.body;
await ResultDAO.updateTags(uid, resultid, tags);
return res.sendStatus(200);
await ResultDAO.updateTags(uid, resultId, tagIds);
return new MonkeyResponse("Result tags updated");
}
static async addResult(req, res) {
static async addResult(req, _res) {
const { uid } = req.ctx.decodedToken;
const { result } = req.body;
result.uid = uid;
if (validateObjectValues(result) > 0)
return res.status(400).json({ message: "Bad input" });
throw new MonkeyError(400, "Bad input");
if (result.wpm === result.raw && result.acc !== 100) {
return res.status(400).json({ message: "Bad input" });
throw new MonkeyError(400, "Bad input");
}
if (
(result.mode === "time" && result.mode2 < 15 && result.mode2 > 0) ||
@ -91,7 +91,7 @@ class ResultController {
result.customText.isTimeRandom &&
result.customText.time < 15)
) {
return res.status(400).json({ message: "Test too short" });
throw new MonkeyError(400, "Test too short");
}
let resulthash = result.hash;
@ -112,15 +112,13 @@ class ResultController {
},
uid
);
return res.status(400).json({ message: "Incorrect result hash" });
throw new MonkeyError(400, "Incorrect result hash");
}
}
if (validateResult) {
if (!validateResult(result)) {
return res
.status(400)
.json({ message: "Result data doesn't make sense" });
throw new MonkeyError(400, "Result data doesn't make sense");
}
} else {
if (process.env.MODE === "dev") {
@ -185,7 +183,7 @@ class ResultController {
},
uid
);
return res.status(400).json({ message: "Invalid result spacing" });
throw new MonkeyError(400, "Invalid result spacing");
}
try {
@ -226,7 +224,7 @@ class ResultController {
) {
if (validateKeys) {
if (!validateKeys(result, uid)) {
return res.status(400).json({ message: "Possible bot detected" });
throw new MonkeyError(400, "Possible bot detected");
}
} else {
if (process.env.MODE === "dev") {
@ -238,7 +236,7 @@ class ResultController {
}
}
} else {
return res.status(400).json({ message: "Missing key data" });
throw new MonkeyError(400, "Missing key data");
}
}
}
@ -318,14 +316,15 @@ class ResultController {
);
}
return res.status(200).json({
message: "Result saved",
const data = {
isPb,
name: result.name,
tagPbs,
insertedId: addedResult.insertedId,
});
};
return new MonkeyResponse("Result saved", data);
}
}
module.exports = ResultController;
export default ResultController;

View file

@ -1,64 +1,58 @@
const UsersDAO = require("../../dao/user");
const BotDAO = require("../../dao/bot");
const { isUsernameValid } = require("../../handlers/validation");
const MonkeyError = require("../../handlers/error");
const fetch = require("node-fetch");
const Logger = require("../../handlers/logger.js");
const uaparser = require("ua-parser-js");
import UsersDAO from "../../dao/user";
import BotDAO from "../../dao/bot";
import { isUsernameValid } from "../../handlers/validation";
import MonkeyError from "../../handlers/error";
import fetch from "node-fetch";
import Logger from "./../../handlers/logger.js";
import uaparser from "ua-parser-js";
import { MonkeyResponse } from "../../handlers/monkey-response";
class UserController {
static async createNewUser(req, res) {
static async createNewUser(req, _res) {
const { name } = req.body;
const { email, uid } = req.ctx.decodedToken;
await UsersDAO.addUser(name, email, uid);
Logger.log("user_created", `${name} ${email}`, uid);
return res.sendStatus(200);
return new MonkeyResponse("User created");
}
static async deleteUser(req, res) {
static async deleteUser(req, _res) {
const { uid } = req.ctx.decodedToken;
const userInfo = await UsersDAO.getUser(uid);
await UsersDAO.deleteUser(uid);
Logger.log("user_deleted", `${userInfo.email} ${userInfo.name}`, uid);
return res.sendStatus(200);
return new MonkeyResponse("User deleted");
}
static async updateName(req, res) {
static async updateName(req, _res) {
const { uid } = req.ctx.decodedToken;
const { name } = req.body;
if (!isUsernameValid(name)) {
return res.status(400).json({
message:
"Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -",
});
}
const olduser = await UsersDAO.getUser(uid);
if (!isUsernameValid(name))
throw new MonkeyError(
400,
"Username invalid. Name cannot contain special characters or contain more than 14 characters. Can include _ . and -"
);
let olduser = await UsersDAO.getUser(uid);
await UsersDAO.updateName(uid, name);
Logger.log(
"user_name_updated",
`changed name from ${olduser.name} to ${name}`,
uid
);
return res.sendStatus(200);
return new MonkeyResponse("User's name updated");
}
static async clearPb(req, res) {
static async clearPb(req, _res) {
const { uid } = req.ctx.decodedToken;
await UsersDAO.clearPb(uid);
Logger.log("user_cleared_pbs", "", uid);
return res.sendStatus(200);
return new MonkeyResponse("User's PB cleared");
}
static async checkName(req, res) {
static async checkName(req, _res) {
const { name } = req.params;
if (!isUsernameValid(name)) {
@ -69,14 +63,11 @@ class UserController {
}
const available = await UsersDAO.isNameAvailable(name);
if (!available) {
return res.status(400).json({ message: "Username unavailable" });
}
return res.sendStatus(200);
if (!available) throw new MonkeyError(400, "Username unavailable");
return new MonkeyResponse("Username available");
}
static async updateEmail(req, res) {
static async updateEmail(req, _res) {
const { uid } = req.ctx.decodedToken;
const { newEmail } = req.body;
@ -86,8 +77,7 @@ class UserController {
throw new MonkeyError(400, e.message, "update email", uid);
}
Logger.log("user_email_updated", `changed email to ${newEmail}`, uid);
return res.sendStatus(200);
return new MonkeyResponse("Email updated");
}
static async getUser(req, _res) {
@ -133,8 +123,7 @@ class UserController {
agent.device.type;
}
Logger.log("user_data_requested", logobj, uid);
return userInfo;
return new MonkeyResponse("User data retrieved", userInfo);
}
static async linkDiscord(req, _res) {
@ -179,14 +168,10 @@ class UserController {
await UsersDAO.linkDiscord(uid, did);
await BotDAO.linkDiscord(uid, did);
Logger.log("user_discord_link", `linked to ${did}`, uid);
return {
message: "Discord account linked",
did,
};
return new MonkeyResponse("Discord account linked ", did);
}
static async unlinkDiscord(req, res) {
static async unlinkDiscord(req, _res) {
const { uid } = req.ctx.decodedToken;
let userInfo;
@ -201,58 +186,51 @@ class UserController {
await BotDAO.unlinkDiscord(uid, userInfo.discordId);
await UsersDAO.unlinkDiscord(uid);
Logger.log("user_discord_unlinked", userInfo.discordId, uid);
return res.sendStatus(200);
return new MonkeyResponse("Discord account unlinked ");
}
static async addTag(req, _res) {
const { uid } = req.ctx.decodedToken;
const { tagName } = req.body;
return await UsersDAO.addTag(uid, tagName);
let tag = await UsersDAO.addTag(uid, tagName);
return new MonkeyResponse("Tag updated", tag);
}
static async clearTagPb(req, res) {
static async clearTagPb(req, _res) {
const { uid } = req.ctx.decodedToken;
const { tagId } = req.params;
await UsersDAO.removeTagPb(uid, tagId);
return res.sendStatus(200);
return new MonkeyResponse("Tag PB cleared");
}
static async editTag(req, res) {
static async editTag(req, _res) {
const { uid } = req.ctx.decodedToken;
const { tagId, newName } = req.body;
await UsersDAO.editTag(uid, tagId, newName);
return res.sendStatus(200);
return new MonkeyResponse("Tag updated");
}
static async removeTag(req, res) {
static async removeTag(req, _res) {
const { uid } = req.ctx.decodedToken;
const { tagId } = req.params;
await UsersDAO.removeTag(uid, tagId);
return res.sendStatus(200);
return new MonkeyResponse("Tag deleted");
}
static async getTags(req, _res) {
const { uid } = req.ctx.decodedToken;
return await UsersDAO.getTags(uid);
let tags = await UsersDAO.getTags(uid);
if (tags == undefined) tags = [];
return new MonkeyResponse("Tags retrieved", tags);
}
static async updateLbMemory(req, res) {
static async updateLbMemory(req, _res) {
const { uid } = req.ctx.decodedToken;
const { mode, mode2, language, rank } = req.body;
await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank);
return res.sendStatus(200);
return new MonkeyResponse("Leaderboard memory updated");
}
}
module.exports = UserController;
export default UserController;

View file

@ -1,32 +0,0 @@
const { Router } = require("express");
const { authenticateRequest } = require("../../middlewares/auth");
const {
asyncHandler,
validateRequest,
} = require("../../middlewares/api-utils");
const configSchema = require("../schemas/config-schema");
const ConfigController = require("../controllers/config");
const RateLimit = require("../../middlewares/rate-limit");
const router = Router();
router.get(
"/",
RateLimit.configGet,
authenticateRequest(),
asyncHandler(ConfigController.getConfig)
);
router.post(
"/save",
RateLimit.configUpdate,
authenticateRequest(),
validateRequest({
body: {
config: configSchema,
},
}),
asyncHandler(ConfigController.saveConfig)
);
module.exports = router;

View file

@ -0,0 +1,29 @@
import { Router } from "express";
import { authenticateRequest } from "../../middlewares/auth";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import configSchema from "../schemas/config-schema";
import ConfigController from "../controllers/config";
import * as RateLimit from "../../middlewares/rate-limit";
const router = Router();
router.get(
"/",
RateLimit.configGet,
authenticateRequest(),
asyncHandler(ConfigController.getConfig)
);
router.patch(
"/",
RateLimit.configUpdate,
authenticateRequest(),
validateRequest({
body: {
config: configSchema.required(),
},
}),
asyncHandler(ConfigController.saveConfig)
);
export default router;

View file

@ -1,21 +1,53 @@
import users from "./users";
import configs from "./configs";
import results from "./results";
import presets from "./presets";
import psas from "./psas";
import leaderboards from "./leaderboards";
import quotes from "./quotes";
import { asyncHandler } from "../../middlewares/api-utils";
import { MonkeyResponse } from "../../handlers/monkey-response";
const pathOverride = process.env.API_PATH_OVERRIDE;
const BASE_ROUTE = pathOverride ? `/${pathOverride}` : "";
const APP_START_TIME = Date.now();
let requestsProcessed = 0;
const API_ROUTE_MAP = {
"/user": require("./user"),
"/config": require("./config"),
"/results": require("./result"),
"/presets": require("./preset"),
"/psa": require("./psa"),
"/leaderboard": require("./leaderboards"),
"/quotes": require("./quotes"),
"/users": users,
"/configs": configs,
"/results": results,
"/presets": presets,
"/psas": psas,
"/leaderboards": leaderboards,
"/quotes": quotes,
};
function addApiRoutes(app) {
app.get("/", (req, res) => {
res.status(200).json({ message: "OK" });
app.use((req, res, next) => {
const inMaintenance =
process.env.MAINTENANCE === "true" || req.ctx.configuration.maintenance;
if (inMaintenance) {
return res
.status(503)
.json({ message: "Server is down for maintenance" });
}
requestsProcessed++;
return next();
});
app.get(
"/",
asyncHandler(async (_req, _res) => {
return new MonkeyResponse("ok", {
uptime: Date.now() - APP_START_TIME,
requestsProcessed,
});
})
);
Object.keys(API_ROUTE_MAP).forEach((route) => {
const apiRoute = `${BASE_ROUTE}${route}`;
const router = API_ROUTE_MAP[route];
@ -23,4 +55,4 @@ function addApiRoutes(app) {
});
}
module.exports = addApiRoutes;
export default addApiRoutes;

View file

@ -1,20 +1,16 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const LeaderboardsController = require("../controllers/leaderboards");
const RateLimit = require("../../middlewares/rate-limit");
const {
asyncHandler,
validateRequest,
} = require("../../middlewares/api-utils");
const { Router } = require("express");
import joi from "joi";
import { authenticateRequest } from "../../middlewares/auth";
import LeaderboardsController from "../controllers/leaderboards";
import * as RateLimit from "../../middlewares/rate-limit";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import { Router } from "express";
const router = Router();
router.get(
"/",
RateLimit.leaderboardsGet,
authenticateRequest({ isPublic: true }),
authenticateRequest({ isPublic: true, acceptMonkeyTokens: false }),
validateRequest({
query: {
language: joi.string().required(),
@ -43,4 +39,4 @@ router.get(
asyncHandler(LeaderboardsController.getRank)
);
module.exports = router;
export default router;

View file

@ -1,14 +1,10 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const PresetController = require("../controllers/preset");
const RateLimit = require("../../middlewares/rate-limit");
const configSchema = require("../schemas/config-schema");
const {
asyncHandler,
validateRequest,
} = require("../../middlewares/api-utils");
const { Router } = require("express");
import joi from "joi";
import { authenticateRequest } from "../../middlewares/auth";
import PresetController from "../controllers/preset";
import * as RateLimit from "../../middlewares/rate-limit";
import configSchema from "../schemas/config-schema";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import { Router } from "express";
const router = Router();
@ -30,7 +26,7 @@ router.get(
);
router.post(
"/add",
"/",
RateLimit.presetsAdd,
authenticateRequest(),
validateRequest({
@ -44,8 +40,8 @@ router.post(
asyncHandler(PresetController.addPreset)
);
router.post(
"/edit",
router.patch(
"/",
RateLimit.presetsEdit,
authenticateRequest(),
validateRequest({
@ -62,16 +58,16 @@ router.post(
asyncHandler(PresetController.editPreset)
);
router.post(
"/remove",
router.delete(
"/:presetId",
RateLimit.presetsRemove,
authenticateRequest(),
validateRequest({
body: {
_id: joi.string().required(),
params: {
presetId: joi.string().required(),
},
}),
asyncHandler(PresetController.removePreset)
);
module.exports = router;
export default router;

View file

@ -1,11 +0,0 @@
const PsaController = require("../controllers/psa");
const RateLimit = require("../../middlewares/rate-limit");
const { asyncHandler } = require("../../middlewares/api-utils");
const { Router } = require("express");
const router = Router();
router.get("/", RateLimit.psaGet, asyncHandler(PsaController.get));
module.exports = router;

View file

@ -0,0 +1,10 @@
import PsaController from "../controllers/psa";
import * as RateLimit from "../../middlewares/rate-limit";
import { asyncHandler } from "../../middlewares/api-utils";
import { Router } from "express";
const router = Router();
router.get("/", RateLimit.psaGet, asyncHandler(PsaController.get));
export default router;

View file

@ -1,16 +1,16 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const NewQuotesController = require("../controllers/new-quotes");
const QuoteRatingsController = require("../controllers/quote-ratings");
const QuotesController = require("../controllers/quotes");
const RateLimit = require("../../middlewares/rate-limit");
const {
import joi from "joi";
import { authenticateRequest } from "../../middlewares/auth";
import { Router } from "express";
import NewQuotesController from "../controllers/new-quotes";
import QuoteRatingsController from "../controllers/quote-ratings";
import QuotesController from "../controllers/quotes";
import * as RateLimit from "../../middlewares/rate-limit";
import {
asyncHandler,
validateRequest,
validateConfiguration,
} = require("../../middlewares/api-utils");
const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quote-languages");
validateRequest,
} from "../../middlewares/api-utils";
import SUPPORTED_QUOTE_LANGUAGES from "../../constants/quote-languages";
const quotesRouter = Router();
@ -51,8 +51,8 @@ quotesRouter.post(
validateRequest({
body: {
quoteId: joi.string().required(),
editText: joi.string().required(),
editSource: joi.string().required(),
editText: joi.string().allow(null),
editSource: joi.string().allow(null),
},
validationErrorMessage: "Please fill all the fields",
}),
@ -131,4 +131,4 @@ quotesRouter.post(
asyncHandler(QuotesController.reportQuote)
);
module.exports = quotesRouter;
export default quotesRouter;

View file

@ -1,53 +0,0 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const ResultController = require("../controllers/result");
const RateLimit = require("../../middlewares/rate-limit");
const {
asyncHandler,
validateRequest,
} = require("../../middlewares/api-utils");
const resultSchema = require("../schemas/result-schema");
const router = Router();
router.get(
"/",
RateLimit.resultsGet,
authenticateRequest(),
asyncHandler(ResultController.getResults)
);
router.post(
"/add",
RateLimit.resultsAdd,
authenticateRequest(),
validateRequest({
body: {
result: resultSchema,
},
}),
asyncHandler(ResultController.addResult)
);
router.post(
"/updateTags",
RateLimit.resultsTagsUpdate,
authenticateRequest(),
validateRequest({
body: {
tags: joi.array().items(joi.string()).required(),
resultid: joi.string().required(),
},
}),
asyncHandler(ResultController.updateTags)
);
router.post(
"/deleteAll",
RateLimit.resultsDeleteAll,
authenticateRequest(),
asyncHandler(ResultController.deleteAll)
);
module.exports = router;

View file

@ -0,0 +1,50 @@
import ResultController from "../controllers/result";
import resultSchema from "../schemas/result-schema";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import * as RateLimit from "../../middlewares/rate-limit";
import { Router } from "express";
import { authenticateRequest } from "../../middlewares/auth";
import joi from "joi";
const router = Router();
router.get(
"/",
RateLimit.resultsGet,
authenticateRequest(),
asyncHandler(ResultController.getResults)
);
router.post(
"/",
RateLimit.resultsAdd,
authenticateRequest(),
validateRequest({
body: {
result: resultSchema,
},
}),
asyncHandler(ResultController.addResult)
);
router.patch(
"/tags",
RateLimit.resultsTagsUpdate,
authenticateRequest(),
validateRequest({
body: {
tagIds: joi.array().items(joi.string()).required(),
resultId: joi.string().required(),
},
}),
asyncHandler(ResultController.updateTags)
);
router.delete(
"/",
RateLimit.resultsDeleteAll,
authenticateRequest(),
asyncHandler(ResultController.deleteAll)
);
export default router;

View file

@ -1,12 +1,9 @@
const joi = require("joi");
const { authenticateRequest } = require("../../middlewares/auth");
const { Router } = require("express");
const UserController = require("../controllers/user");
const RateLimit = require("../../middlewares/rate-limit");
const {
asyncHandler,
validateRequest,
} = require("../../middlewares/api-utils");
import joi from "joi";
import { authenticateRequest } from "../../middlewares/auth";
import { Router } from "express";
import UserController from "../controllers/user";
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
import * as RateLimit from "../../middlewares/rate-limit";
const router = Router();
@ -96,7 +93,6 @@ router.patch(
authenticateRequest(),
validateRequest({
body: {
uid: joi.string().required(),
newEmail: joi.string().email().required(),
previousEmail: joi.string().email().required(),
},
@ -176,7 +172,7 @@ router.post(
data: joi.object({
tokenType: joi.string().required(),
accessToken: joi.string().required(),
uid: joi.string().required(),
uid: joi.string(),
}),
},
}),
@ -190,4 +186,4 @@ router.post(
asyncHandler(UserController.unlinkDiscord)
);
module.exports = router;
export default router;

View file

@ -1,5 +1,5 @@
const _ = require("lodash");
const joi = require("joi");
import _ from "lodash";
import joi from "joi";
const CARET_STYLES = [
"off",
@ -107,4 +107,4 @@ const CONFIG_SCHEMA = joi.object({
showAvg: joi.boolean(),
});
module.exports = CONFIG_SCHEMA;
export default CONFIG_SCHEMA;

View file

@ -1,4 +1,4 @@
const joi = require("joi");
import joi from "joi";
const RESULT_SCHEMA = joi
.object({
@ -63,4 +63,4 @@ const RESULT_SCHEMA = joi
})
.required();
module.exports = RESULT_SCHEMA;
export default RESULT_SCHEMA;

27
backend/app.js Normal file
View file

@ -0,0 +1,27 @@
import cors from "cors";
import helmet from "helmet";
import addApiRoutes from "./api/routes";
import express, { urlencoded, json } from "express";
import contextMiddleware from "./middlewares/context";
import errorHandlingMiddleware from "./middlewares/error";
function buildApp() {
const app = express();
app.use(urlencoded({ extended: true }));
app.use(json());
app.use(cors());
app.use(helmet());
app.set("trust proxy", 1);
app.use(contextMiddleware);
addApiRoutes(app);
app.use(errorHandlingMiddleware);
return app;
}
export default buildApp();

View file

@ -3,7 +3,7 @@
* To add a new configuration. Simply add it to this object.
* When changing this template, please follow the principle of "Secure by default" (https://en.wikipedia.org/wiki/Secure_by_default).
*/
const BASE_CONFIGURATION = Object.freeze({
const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
maintenance: false,
quoteReport: {
enabled: false,
@ -16,6 +16,9 @@ const BASE_CONFIGURATION = Object.freeze({
resultObjectHashCheck: {
enabled: false,
},
});
monkeyTokens: {
enabled: false,
},
};
module.exports = BASE_CONFIGURATION;
export default Object.freeze(BASE_CONFIGURATION);

View file

@ -34,4 +34,4 @@ const SUPPORTED_QUOTE_LANGUAGES = [
"vietnamese",
];
module.exports = SUPPORTED_QUOTE_LANGUAGES;
export default SUPPORTED_QUOTE_LANGUAGES;

View file

@ -1,7 +1,7 @@
const { mongoDB } = require("../init/mongodb");
import db from "../init/db";
async function addCommand(command, commandArguments) {
return await mongoDB().collection("bot-commands").insertOne({
return await db.collection("bot-commands").insertOne({
command,
arguments: commandArguments,
executed: false,
@ -23,9 +23,7 @@ async function addCommands(commands, commandArguments) {
};
});
return await mongoDB()
.collection("bot-commands")
.insertMany(normalizedCommands);
return await db.collection("bot-commands").insertMany(normalizedCommands);
}
class BotDAO {
@ -67,4 +65,4 @@ class BotDAO {
}
}
module.exports = BotDAO;
export default BotDAO;

View file

@ -1,17 +1,17 @@
const { mongoDB } = require("../init/mongodb");
import db from "../init/db";
class ConfigDAO {
static async saveConfig(uid, config) {
return await mongoDB()
return await db
.collection("configs")
.updateOne({ uid }, { $set: { config } }, { upsert: true });
}
static async getConfig(uid) {
let config = await mongoDB().collection("configs").findOne({ uid });
let config = await db.collection("configs").findOne({ uid });
// if (!config) throw new MonkeyError(404, "Config not found");
return config;
}
}
module.exports = ConfigDAO;
export default ConfigDAO;

View file

@ -1,7 +1,7 @@
const _ = require("lodash");
const db = require("../init/db");
const BASE_CONFIGURATION = require("../constants/base-configuration");
const Logger = require("../handlers/logger.js");
import _ from "lodash";
import db from "../init/db";
import BASE_CONFIGURATION from "../constants/base-configuration";
import Logger from "../handlers/logger.js";
const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes
@ -113,4 +113,4 @@ class ConfigurationDAO {
}
}
module.exports = ConfigurationDAO;
export default ConfigurationDAO;

View file

@ -1,6 +1,6 @@
const { mongoDB } = require("../init/mongodb");
const Logger = require("../handlers/logger");
const { performance } = require("perf_hooks");
import { mongoDB } from "../init/mongodb";
import Logger from "../handlers/logger";
import { performance } from "perf_hooks";
class LeaderboardsDAO {
static async get(mode, mode2, language, skip, limit = 50) {
@ -11,7 +11,7 @@ class LeaderboardsDAO {
.find()
.sort({ rank: 1 })
.skip(parseInt(skip))
.limit(parseInt(limit))
.limit(parseInt(limit.toString()))
.toArray();
return preset;
}
@ -131,4 +131,4 @@ class LeaderboardsDAO {
}
}
module.exports = LeaderboardsDAO;
export default LeaderboardsDAO;

View file

@ -1,16 +1,18 @@
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const fs = require("fs");
const simpleGit = require("simple-git");
const path = require("path");
import simpleGit from "simple-git";
import Mongo from "mongodb";
const { ObjectID } = Mongo;
import stringSimilarity from "string-similarity";
import path from "path";
import fs from "fs";
import db from "../init/db";
import MonkeyError from "../handlers/error";
let git;
try {
git = simpleGit(path.join(__dirname, "../../../monkeytype-new-quotes"));
} catch (e) {
git = undefined;
}
const stringSimilarity = require("string-similarity");
const { ObjectID } = require("mongodb");
class NewQuotesDAO {
static async add(text, source, language, uid) {
@ -50,12 +52,12 @@ class NewQuotesDAO {
if (duplicateId != -1) {
return { duplicateId, similarityScore };
}
return await mongoDB().collection("new-quotes").insertOne(quote);
return await db.collection("new-quotes").insertOne(quote);
}
static async get() {
if (!git) throw new MonkeyError(500, "Git not available.");
return await mongoDB()
return await db
.collection("new-quotes")
.find({ approved: false })
.sort({ timestamp: 1 })
@ -66,7 +68,7 @@ class NewQuotesDAO {
static async approve(quoteId, editQuote, editSource) {
if (!git) throw new MonkeyError(500, "Git not available.");
//check mod status
let quote = await mongoDB()
let quote = await db
.collection("new-quotes")
.findOne({ _id: ObjectID(quoteId) });
if (!quote) {
@ -86,21 +88,21 @@ class NewQuotesDAO {
await git.pull("upstream", "master");
if (fs.existsSync(fileDir)) {
let quoteFile = fs.readFileSync(fileDir);
quoteFile = JSON.parse(quoteFile.toString());
quoteFile.quotes.every((old) => {
const quoteObject = JSON.parse(quoteFile.toString());
quoteObject.quotes.every((old) => {
if (stringSimilarity.compareTwoStrings(old.text, quote.text) > 0.8) {
throw new MonkeyError(409, "Duplicate quote");
}
});
let maxid = 0;
quoteFile.quotes.map(function (q) {
quoteObject.quotes.map(function (q) {
if (q.id > maxid) {
maxid = q.id;
}
});
quote.id = maxid + 1;
quoteFile.quotes.push(quote);
fs.writeFileSync(fileDir, JSON.stringify(quoteFile, null, 2));
quoteObject.quotes.push(quote);
fs.writeFileSync(fileDir, JSON.stringify(quoteObject, null, 2));
message = `Added quote to ${language}.json.`;
} else {
//file doesnt exist, create it
@ -123,18 +125,16 @@ class NewQuotesDAO {
await git.add([`static/quotes/${language}.json`]);
await git.commit(`Added quote to ${language}.json`);
await git.push("origin", "master");
await mongoDB()
.collection("new-quotes")
.deleteOne({ _id: ObjectID(quoteId) });
await db.collection("new-quotes").deleteOne({ _id: ObjectID(quoteId) });
return { quote, message };
}
static async refuse(quoteId) {
if (!git) throw new MonkeyError(500, "Git not available.");
return await mongoDB()
return await db
.collection("new-quotes")
.deleteOne({ _id: ObjectID(quoteId) });
}
}
module.exports = NewQuotesDAO;
export default NewQuotesDAO;

View file

@ -1,10 +1,11 @@
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { ObjectID } = require("mongodb");
import MonkeyError from "../handlers/error";
import db from "../init/db";
import Mongo from "mongodb";
const { ObjectID } = Mongo;
class PresetDAO {
static async getPresets(uid) {
const preset = await mongoDB()
const preset = await db
.collection("presets")
.find({ uid })
.sort({ timestamp: -1 })
@ -13,9 +14,9 @@ class PresetDAO {
}
static async addPreset(uid, name, config) {
const count = await mongoDB().collection("presets").find({ uid }).count();
const count = await db.collection("presets").find({ uid }).count();
if (count >= 10) throw new MonkeyError(409, "Too many presets");
let preset = await mongoDB()
let preset = await db
.collection("presets")
.insertOne({ uid, name, config });
return {
@ -25,30 +26,30 @@ class PresetDAO {
static async editPreset(uid, _id, name, config) {
console.log(_id);
const preset = await mongoDB()
const preset = await db
.collection("presets")
.findOne({ uid, _id: ObjectID(_id) });
if (!preset) throw new MonkeyError(404, "Preset not found");
if (config) {
return await mongoDB()
return await db
.collection("presets")
.updateOne({ uid, _id: ObjectID(_id) }, { $set: { name, config } });
} else {
return await mongoDB()
return await db
.collection("presets")
.updateOne({ uid, _id: ObjectID(_id) }, { $set: { name } });
}
}
static async removePreset(uid, _id) {
const preset = await mongoDB()
const preset = await db
.collection("presets")
.findOne({ uid, _id: ObjectID(_id) });
if (!preset) throw new MonkeyError(404, "Preset not found");
return await mongoDB()
return await db
.collection("presets")
.deleteOne({ uid, _id: ObjectID(_id) });
}
}
module.exports = PresetDAO;
export default PresetDAO;

View file

@ -1,9 +1,9 @@
const { mongoDB } = require("../init/mongodb");
import db from "../init/db";
class PsaDAO {
static async get(_uid, _config) {
return await mongoDB().collection("psa").find().toArray();
return await db.collection("psa").find().toArray();
}
}
module.exports = PsaDAO;
export default PsaDAO;

View file

@ -1,26 +1,24 @@
// const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { roundTo2 } = require("../handlers/misc");
import db from "../init/db";
import { roundTo2 } from "../handlers/misc";
class PublicStatsDAO {
//needs to be rewritten, this is public stats not user stats
static async updateStats(restartCount, time) {
time = roundTo2(time);
await mongoDB()
.collection("public")
.updateOne(
{ type: "stats" },
{
$inc: {
testsCompleted: 1,
testsStarted: restartCount + 1,
timeTyping: time,
},
await db.collection("public").updateOne(
{ type: "stats" },
{
$inc: {
testsCompleted: 1,
testsStarted: restartCount + 1,
timeTyping: time,
},
{ upsert: true }
);
},
{ upsert: true }
);
return true;
}
}
module.exports = PublicStatsDAO;
export default PublicStatsDAO;

View file

@ -1,9 +1,9 @@
const { mongoDB } = require("../init/mongodb");
import db from "../init/db";
class QuoteRatingsDAO {
static async submit(quoteId, language, rating, update) {
if (update) {
await mongoDB()
await db
.collection("quote-rating")
.updateOne(
{ quoteId, language },
@ -11,7 +11,7 @@ class QuoteRatingsDAO {
{ upsert: true }
);
} else {
await mongoDB()
await db
.collection("quote-rating")
.updateOne(
{ quoteId, language },
@ -27,16 +27,14 @@ class QuoteRatingsDAO {
).toFixed(1)
);
return await mongoDB()
return await db
.collection("quote-rating")
.updateOne({ quoteId, language }, { $set: { average } });
}
static async get(quoteId, language) {
return await mongoDB()
.collection("quote-rating")
.findOne({ quoteId, language });
return await db.collection("quote-rating").findOne({ quoteId, language });
}
}
module.exports = QuoteRatingsDAO;
export default QuoteRatingsDAO;

View file

@ -1,12 +1,12 @@
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
import MonkeyError from "../handlers/error";
import db from "../init/db";
const MAX_REPORTS = 1000;
const CONTENT_REPORT_LIMIT = 5;
class ReportDAO {
static async createReport(report) {
const reports = await mongoDB().collection("reports").find().toArray();
const reports = await db.collection("reports").find().toArray();
if (reports.length >= MAX_REPORTS) {
throw new MonkeyError(
@ -26,8 +26,8 @@ class ReportDAO {
);
}
await mongoDB().collection("reports").insertOne(report);
await db.collection("reports").insertOne(report);
}
}
module.exports = ReportDAO;
export default ReportDAO;

View file

@ -1,7 +1,9 @@
const { ObjectID } = require("mongodb");
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const UserDAO = require("./user");
import Mongo from "mongodb";
const { ObjectID } = Mongo;
import MonkeyError from "../handlers/error";
import db from "../init/db";
import UserDAO from "./user";
class ResultDAO {
static async addResult(uid, result) {
@ -14,18 +16,18 @@ class ResultDAO {
if (!user) throw new MonkeyError(404, "User not found", "add result");
if (result.uid === undefined) result.uid = uid;
// result.ir = true;
let res = await mongoDB().collection("results").insertOne(result);
let res = await db.collection("results").insertOne(result);
return {
insertedId: res.insertedId,
};
}
static async deleteAll(uid) {
return await mongoDB().collection("results").deleteMany({ uid });
return await db.collection("results").deleteMany({ uid });
}
static async updateTags(uid, resultid, tags) {
const result = await mongoDB()
const result = await db
.collection("results")
.findOne({ _id: ObjectID(resultid), uid });
if (!result) throw new MonkeyError(404, "Result not found");
@ -37,13 +39,13 @@ class ResultDAO {
});
if (!validTags)
throw new MonkeyError(400, "One of the tag id's is not valid");
return await mongoDB()
return await db
.collection("results")
.updateOne({ _id: ObjectID(resultid), uid }, { $set: { tags } });
}
static async getResult(uid, id) {
const result = await mongoDB()
const result = await db
.collection("results")
.findOne({ _id: ObjectID(id), uid });
if (!result) throw new MonkeyError(404, "Result not found");
@ -51,7 +53,7 @@ class ResultDAO {
}
static async getLastResult(uid) {
let result = await mongoDB()
let result = await db
.collection("results")
.find({ uid })
.sort({ timestamp: -1 })
@ -63,13 +65,13 @@ class ResultDAO {
}
static async getResultByTimestamp(uid, timestamp) {
return await mongoDB().collection("results").findOne({ uid, timestamp });
return await db.collection("results").findOne({ uid, timestamp });
}
static async getResults(uid, start, end) {
start = start ?? 0;
end = end ?? 1000;
const result = await mongoDB()
const result = await db
.collection("results")
.find({ uid })
.sort({ timestamp: -1 })
@ -81,4 +83,4 @@ class ResultDAO {
}
}
module.exports = ResultDAO;
export default ResultDAO;

View file

@ -1,49 +1,51 @@
const MonkeyError = require("../handlers/error");
const { mongoDB } = require("../init/mongodb");
const { ObjectID } = require("mongodb");
const { checkAndUpdatePb } = require("../handlers/pb");
const { updateAuthEmail } = require("../handlers/auth");
const { isUsernameValid } = require("../handlers/validation");
import { isUsernameValid } from "../handlers/validation";
import { updateAuthEmail } from "../handlers/auth";
import { checkAndUpdatePb } from "../handlers/pb";
import db from "../init/db";
import MonkeyError from "../handlers/error";
import Mongo from "mongodb";
const { ObjectID } = Mongo;
class UsersDAO {
static async addUser(name, email, uid) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (user)
throw new MonkeyError(400, "User document already exists", "addUser");
return await mongoDB()
return await db
.collection("users")
.insertOne({ name, email, uid, addedAt: Date.now() });
}
static async deleteUser(uid) {
return await mongoDB().collection("users").deleteOne({ uid });
return await db.collection("users").deleteOne({ uid });
}
static async updateName(uid, name) {
const nameDoc = await mongoDB()
const nameDoc = await db
.collection("users")
.findOne({ name: { $regex: new RegExp(`^${name}$`, "i") } });
if (nameDoc) throw new MonkeyError(409, "Username already taken", name);
let user = await mongoDB().collection("users").findOne({ uid });
let user = await db.collection("users").findOne({ uid });
if (
Date.now() - user.lastNameChange < 2592000000 &&
isUsernameValid(user.name)
) {
throw new MonkeyError(409, "You can change your name once every 30 days");
}
return await mongoDB()
return await db
.collection("users")
.updateOne({ uid }, { $set: { name, lastNameChange: Date.now() } });
}
static async clearPb(uid) {
return await mongoDB()
return await db
.collection("users")
.updateOne({ uid }, { $set: { personalBests: {}, lbPersonalBests: {} } });
}
static async isNameAvailable(name) {
const nameDoc = await mongoDB().collection("users").findOne({ name });
const nameDoc = await db.collection("users").findOne({ name });
if (nameDoc) {
return false;
} else {
@ -52,31 +54,29 @@ class UsersDAO {
}
static async updateQuoteRatings(uid, quoteRatings) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user)
throw new MonkeyError(404, "User not found", "updateQuoteRatings");
await mongoDB()
.collection("users")
.updateOne({ uid }, { $set: { quoteRatings } });
await db.collection("users").updateOne({ uid }, { $set: { quoteRatings } });
return true;
}
static async updateEmail(uid, email) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "update email");
await updateAuthEmail(uid, email);
await mongoDB().collection("users").updateOne({ uid }, { $set: { email } });
await db.collection("users").updateOne({ uid }, { $set: { email } });
return true;
}
static async getUser(uid) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "get user");
return user;
}
static async getUserByDiscordId(discordId) {
const user = await mongoDB().collection("users").findOne({ discordId });
const user = await db.collection("users").findOne({ discordId });
if (!user)
throw new MonkeyError(404, "User not found", "get user by discord id");
return user;
@ -84,7 +84,7 @@ class UsersDAO {
static async addTag(uid, name) {
let _id = ObjectID();
await mongoDB()
await db
.collection("users")
.updateOne({ uid }, { $push: { tags: { _id, name } } });
return {
@ -94,88 +94,80 @@ class UsersDAO {
}
static async getTags(uid) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
// if (!user) throw new MonkeyError(404, "User not found", "get tags");
return user?.tags ?? [];
}
static async editTag(uid, _id, name) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "edit tag");
if (
user.tags === undefined ||
user.tags.filter((t) => t._id == _id).length === 0
)
throw new MonkeyError(404, "Tag not found");
return await mongoDB()
.collection("users")
.updateOne(
{
uid: uid,
"tags._id": ObjectID(_id),
},
{ $set: { "tags.$.name": name } }
);
return await db.collection("users").updateOne(
{
uid: uid,
"tags._id": ObjectID(_id),
},
{ $set: { "tags.$.name": name } }
);
}
static async removeTag(uid, _id) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "remove tag");
if (
user.tags === undefined ||
user.tags.filter((t) => t._id == _id).length === 0
)
throw new MonkeyError(404, "Tag not found");
return await mongoDB()
.collection("users")
.updateOne(
{
uid: uid,
"tags._id": ObjectID(_id),
},
{ $pull: { tags: { _id: ObjectID(_id) } } }
);
return await db.collection("users").updateOne(
{
uid: uid,
"tags._id": ObjectID(_id),
},
{ $pull: { tags: { _id: ObjectID(_id) } } }
);
}
static async removeTagPb(uid, _id) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "remove tag pb");
if (
user.tags === undefined ||
user.tags.filter((t) => t._id == _id).length === 0
)
throw new MonkeyError(404, "Tag not found");
return await mongoDB()
.collection("users")
.updateOne(
{
uid: uid,
"tags._id": ObjectID(_id),
},
{ $set: { "tags.$.personalBests": {} } }
);
return await db.collection("users").updateOne(
{
uid: uid,
"tags._id": ObjectID(_id),
},
{ $set: { "tags.$.personalBests": {} } }
);
}
static async updateLbMemory(uid, mode, mode2, language, rank) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "update lb memory");
if (user.lbMemory === undefined) user.lbMemory = {};
if (user.lbMemory[mode] === undefined) user.lbMemory[mode] = {};
if (user.lbMemory[mode][mode2] === undefined)
user.lbMemory[mode][mode2] = {};
user.lbMemory[mode][mode2][language] = rank;
return await mongoDB()
.collection("users")
.updateOne(
{ uid },
{
$set: { lbMemory: user.lbMemory },
}
);
return await db.collection("users").updateOne(
{ uid },
{
$set: { lbMemory: user.lbMemory },
}
);
}
static async checkIfPb(uid, result) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "check if pb");
const {
@ -219,11 +211,11 @@ class UsersDAO {
);
if (pb.isPb) {
await mongoDB()
await db
.collection("users")
.updateOne({ uid }, { $set: { personalBests: pb.obj } });
if (pb.lbObj) {
await mongoDB()
await db
.collection("users")
.updateOne({ uid }, { $set: { lbPersonalBests: pb.lbObj } });
}
@ -234,7 +226,7 @@ class UsersDAO {
}
static async checkIfTagPb(uid, result) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "check if tag pb");
if (user.tags === undefined || user.tags.length === 0) {
@ -292,7 +284,7 @@ class UsersDAO {
);
if (tagpb.isPb) {
ret.push(tag._id);
await mongoDB()
await db
.collection("users")
.updateOne(
{ uid, "tags._id": ObjectID(tag._id) },
@ -305,50 +297,48 @@ class UsersDAO {
}
static async resetPb(uid) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "reset pb");
return await mongoDB()
return await db
.collection("users")
.updateOne({ uid }, { $set: { personalBests: {} } });
}
static async updateTypingStats(uid, restartCount, timeTyping) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user)
throw new MonkeyError(404, "User not found", "update typing stats");
return await mongoDB()
.collection("users")
.updateOne(
{ uid },
{
$inc: {
startedTests: restartCount + 1,
completedTests: 1,
timeTyping,
},
}
);
return await db.collection("users").updateOne(
{ uid },
{
$inc: {
startedTests: restartCount + 1,
completedTests: 1,
timeTyping,
},
}
);
}
static async linkDiscord(uid, discordId) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "link discord");
return await mongoDB()
return await db
.collection("users")
.updateOne({ uid }, { $set: { discordId } });
}
static async unlinkDiscord(uid) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user) throw new MonkeyError(404, "User not found", "unlink discord");
return await mongoDB()
return await db
.collection("users")
.updateOne({ uid }, { $set: { discordId: null } });
}
static async incrementBananas(uid, wpm) {
const user = await mongoDB().collection("users").findOne({ uid });
const user = await db.collection("users").findOne({ uid });
if (!user)
throw new MonkeyError(404, "User not found", "increment bananas");
@ -361,7 +351,7 @@ class UsersDAO {
if (best60 === undefined || wpm >= best60 - best60 * 0.25) {
//increment when no record found or wpm is within 25% of the record
return await mongoDB()
return await db
.collection("users")
.updateOne({ uid }, { $inc: { bananas: 1 } });
} else {
@ -370,4 +360,4 @@ class UsersDAO {
}
}
module.exports = UsersDAO;
export default UsersDAO;

View file

@ -1,13 +1,12 @@
const admin = require("firebase-admin");
import admin from "firebase-admin";
module.exports = {
async verifyIdToken(idToken) {
return await admin.auth().verifyIdToken(idToken, true);
},
async updateAuthEmail(uid, email) {
return await admin.auth().updateUser(uid, {
email,
emailVerified: false,
});
},
};
export async function verifyIdToken(idToken) {
return await admin.auth().verifyIdToken(idToken, true);
}
export async function updateAuthEmail(uid, email) {
return await admin.auth().updateUser(uid, {
email,
emailVerified: false,
});
}

View file

@ -1,20 +1,16 @@
const fetch = require("node-fetch");
const path = require("path");
const { config } = require("dotenv");
config({ path: path.join(__dirname, ".env") });
import fetch from "node-fetch";
import "dotenv/config";
module.exports = {
async verify(captcha) {
if (process.env.MODE === "dev") return true;
let response = await fetch(
`https://www.google.com/recaptcha/api/siteverify`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `secret=${process.env.RECAPTCHA_SECRET}&response=${captcha}`,
}
);
response = await response.json();
return response?.success;
},
};
export async function verify(captcha) {
if (process.env.MODE === "dev") return true;
let response = await fetch(
`https://www.google.com/recaptcha/api/siteverify`,
{
method: "POST",
headers: { "Content-Type": "application/x-www-form-urlencoded" },
body: `secret=${process.env.RECAPTCHA_SECRET}&response=${captcha}`,
}
);
response = await response.json();
return response?.success;
}

View file

@ -1,19 +1,11 @@
const uuid = require("uuid");
import * as uuid from "uuid";
class MonkeyError {
constructor(status, message, stack = null, uid = null) {
this.status = status ?? 500;
this.errorID = uuid.v4();
this.errorId = uuid.v4();
this.stack = stack;
this.uid = uid;
// this.message =
// process.env.MODE === "dev"
// ? stack
// ? String(stack)
// : this.status === 500
// ? String(message)
// : message
// : "Internal Server Error " + this.errorID;
if (process.env.MODE === "dev") {
this.message = stack
@ -22,7 +14,7 @@ class MonkeyError {
} else {
if (this.stack && this.status >= 500) {
this.stack = this.message + "\n" + this.stack;
this.message = "Internal Server Error " + this.errorID;
this.message = "Internal Server Error " + this.errorId;
} else {
this.message = String(message);
}
@ -30,4 +22,4 @@ class MonkeyError {
}
}
module.exports = MonkeyError;
export default MonkeyError;

View file

@ -1,17 +1,15 @@
const db = require("../init/db");
import db from "../init/db.js";
async function log(event, message, uid) {
const logsCollection = db.collection("logs");
export default {
async log(event, message, uid) {
const logsCollection = db.collection("logs");
console.log(new Date(), "\t", event, "\t", uid, "\t", message);
await logsCollection.insertOne({
timestamp: Date.now(),
uid,
event,
message,
});
}
module.exports = {
log,
console.log(new Date(), "\t", event, "\t", uid, "\t", message);
await logsCollection.insertOne({
timestamp: Date.now(),
uid,
event,
message,
});
},
};

View file

@ -1,27 +1,27 @@
module.exports = {
roundTo2(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
},
stdDev(array) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
);
},
mean(array) {
try {
return (
array.reduce((previous, current) => (current += previous)) /
array.length
);
} catch (e) {
return 0;
}
},
kogasa(cov) {
export function roundTo2(num) {
return Math.round((num + Number.EPSILON) * 100) / 100;
}
export function stdDev(array) {
const n = array.length;
const mean = array.reduce((a, b) => a + b) / n;
return Math.sqrt(
array.map((x) => Math.pow(x - mean, 2)).reduce((a, b) => a + b) / n
);
}
export function mean(array) {
try {
return (
100 * (1 - Math.tanh(cov + Math.pow(cov, 3) / 3 + Math.pow(cov, 5) / 5))
array.reduce((previous, current) => (current += previous)) / array.length
);
},
};
} catch (e) {
return 0;
}
}
export function kogasa(cov) {
return (
100 * (1 - Math.tanh(cov + Math.pow(cov, 3) / 3 + Math.pow(cov, 5) / 5))
);
}

View file

@ -0,0 +1,23 @@
export class MonkeyResponse {
constructor(message, data, status = 200) {
this.message = message;
this.data = data ?? null;
this.status = status;
}
}
export function handleMonkeyResponse(handlerData, res) {
const isMonkeyResponse = handlerData instanceof MonkeyResponse;
const monkeyResponse = !isMonkeyResponse
? new MonkeyResponse("ok", handlerData)
: handlerData;
const { message, data, status } = monkeyResponse;
res.status(status);
if ([301, 302].includes(status)) {
return res.redirect(data);
}
res.json({ message, data });
}

View file

@ -40,111 +40,109 @@ custom: {
*/
module.exports = {
checkAndUpdatePb(
export function checkAndUpdatePb(
obj,
lbObj,
mode,
mode2,
acc,
consistency,
difficulty,
lazyMode = false,
language,
punctuation,
raw,
wpm
) {
//verify structure first
if (obj === undefined) obj = {};
if (obj[mode] === undefined) obj[mode] = {};
if (obj[mode][mode2] === undefined) obj[mode][mode2] = [];
let isPb = false;
let found = false;
//find a pb
obj[mode][mode2].forEach((pb) => {
//check if we should compare first
if (
(pb.lazyMode === lazyMode ||
(pb.lazyMode === undefined && lazyMode === false)) &&
pb.difficulty === difficulty &&
pb.language === language &&
pb.punctuation === punctuation
) {
found = true;
//compare
if (pb.wpm < wpm) {
//update
isPb = true;
pb.acc = acc;
pb.consistency = consistency;
pb.difficulty = difficulty;
pb.language = language;
pb.punctuation = punctuation;
pb.lazyMode = lazyMode;
pb.raw = raw;
pb.wpm = wpm;
pb.timestamp = Date.now();
}
}
});
//if not found push a new one
if (!found) {
isPb = true;
obj[mode][mode2].push({
acc,
consistency,
difficulty,
lazyMode,
language,
punctuation,
raw,
wpm,
timestamp: Date.now(),
});
}
if (
lbObj &&
mode === "time" &&
(mode2 == "15" || mode2 == "60") &&
!lazyMode
) {
//updating lbpersonalbests object
//verify structure first
if (lbObj[mode] === undefined) lbObj[mode] = {};
if (lbObj[mode][mode2] === undefined || Array.isArray(lbObj[mode][mode2]))
lbObj[mode][mode2] = {};
let bestForEveryLanguage = {};
if (obj?.[mode]?.[mode2]) {
obj[mode][mode2].forEach((pb) => {
if (!bestForEveryLanguage[pb.language]) {
bestForEveryLanguage[pb.language] = pb;
} else {
if (bestForEveryLanguage[pb.language].wpm < pb.wpm) {
bestForEveryLanguage[pb.language] = pb;
}
}
});
Object.keys(bestForEveryLanguage).forEach((key) => {
if (lbObj[mode][mode2][key] === undefined) {
lbObj[mode][mode2][key] = bestForEveryLanguage[key];
} else {
if (lbObj[mode][mode2][key].wpm < bestForEveryLanguage[key].wpm) {
lbObj[mode][mode2][key] = bestForEveryLanguage[key];
}
}
});
bestForEveryLanguage = {};
}
}
return {
isPb,
obj,
lbObj,
mode,
mode2,
acc,
consistency,
difficulty,
lazyMode = false,
language,
punctuation,
raw,
wpm
) {
//verify structure first
if (obj === undefined) obj = {};
if (obj[mode] === undefined) obj[mode] = {};
if (obj[mode][mode2] === undefined) obj[mode][mode2] = [];
let isPb = false;
let found = false;
//find a pb
obj[mode][mode2].forEach((pb) => {
//check if we should compare first
if (
(pb.lazyMode === lazyMode ||
(pb.lazyMode === undefined && lazyMode === false)) &&
pb.difficulty === difficulty &&
pb.language === language &&
pb.punctuation === punctuation
) {
found = true;
//compare
if (pb.wpm < wpm) {
//update
isPb = true;
pb.acc = acc;
pb.consistency = consistency;
pb.difficulty = difficulty;
pb.language = language;
pb.punctuation = punctuation;
pb.lazyMode = lazyMode;
pb.raw = raw;
pb.wpm = wpm;
pb.timestamp = Date.now();
}
}
});
//if not found push a new one
if (!found) {
isPb = true;
obj[mode][mode2].push({
acc,
consistency,
difficulty,
lazyMode,
language,
punctuation,
raw,
wpm,
timestamp: Date.now(),
});
}
if (
lbObj &&
mode === "time" &&
(mode2 == "15" || mode2 == "60") &&
!lazyMode
) {
//updating lbpersonalbests object
//verify structure first
if (lbObj[mode] === undefined) lbObj[mode] = {};
if (lbObj[mode][mode2] === undefined || Array.isArray(lbObj[mode][mode2]))
lbObj[mode][mode2] = {};
let bestForEveryLanguage = {};
if (obj?.[mode]?.[mode2]) {
obj[mode][mode2].forEach((pb) => {
if (!bestForEveryLanguage[pb.language]) {
bestForEveryLanguage[pb.language] = pb;
} else {
if (bestForEveryLanguage[pb.language].wpm < pb.wpm) {
bestForEveryLanguage[pb.language] = pb;
}
}
});
Object.keys(bestForEveryLanguage).forEach((key) => {
if (lbObj[mode][mode2][key] === undefined) {
lbObj[mode][mode2][key] = bestForEveryLanguage[key];
} else {
if (lbObj[mode][mode2][key].wpm < bestForEveryLanguage[key].wpm) {
lbObj[mode][mode2][key] = bestForEveryLanguage[key];
}
}
});
bestForEveryLanguage = {};
}
}
return {
isPb,
obj,
lbObj,
};
},
};
};
}

View file

@ -1,6 +1,6 @@
const MonkeyError = require("./error");
import MonkeyError from "./error";
function isUsernameValid(name) {
export function isUsernameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (/.*miodec.*/.test(name.toLowerCase())) return false;
//sorry for the bad words
@ -15,7 +15,7 @@ function isUsernameValid(name) {
return /^[0-9a-zA-Z_.-]+$/.test(name);
}
function isTagPresetNameValid(name) {
export function isTagPresetNameValid(name) {
if (name === null || name === undefined || name === "") return false;
if (name.length > 16) return false;
return /^[0-9a-zA-Z_.-]+$/.test(name);
@ -27,7 +27,7 @@ function isConfigKeyValid(name) {
return /^[0-9a-zA-Z_.\-#+]+$/.test(name);
}
function validateConfig(config) {
export function validateConfig(config) {
Object.keys(config).forEach((key) => {
if (!isConfigKeyValid(key)) {
throw new MonkeyError(500, `Invalid config: ${key} failed regex check`);
@ -66,7 +66,7 @@ function validateConfig(config) {
return true;
}
function validateObjectValues(val) {
export function validateObjectValues(val) {
let errCount = 0;
if (val === null || val === undefined) {
//
@ -87,10 +87,3 @@ function validateObjectValues(val) {
}
return errCount;
}
module.exports = {
isUsernameValid,
isTagPresetNameValid,
validateConfig,
validateObjectValues,
};

View file

@ -1,4 +1,4 @@
const { MongoClient } = require("mongodb");
import MongoClient from "mongodb/lib/mongo_client.js";
class DatabaseClient {
static mongoClient = null;
@ -72,4 +72,4 @@ class DatabaseClient {
}
}
module.exports = DatabaseClient;
export default DatabaseClient;

View file

@ -1,7 +1,5 @@
const db = require("./db");
import db from "./db";
module.exports = {
mongoDB() {
return db;
},
};
export function mongoDB() {
return db;
}

View file

@ -1,13 +1,13 @@
const { CronJob } = require("cron");
const { mongoDB } = require("../init/mongodb");
const Logger = require("../handlers/logger");
import { CronJob } from "cron";
import db from "../init/db";
import Logger from "../handlers/logger";
const CRON_SCHEDULE = "0 0 0 * * *";
const LOG_MAX_AGE_DAYS = 7;
const LOG_MAX_AGE_MILLISECONDS = LOG_MAX_AGE_DAYS * 24 * 60 * 60 * 1000;
async function deleteOldLogs() {
const data = await mongoDB()
const data = await db
.collection("logs")
.deleteMany({ timestamp: { $lt: Date.now() - LOG_MAX_AGE_MILLISECONDS } });
@ -18,4 +18,4 @@ async function deleteOldLogs() {
);
}
module.exports = new CronJob(CRON_SCHEDULE, deleteOldLogs);
export default new CronJob(CRON_SCHEDULE, deleteOldLogs);

View file

@ -1,4 +1,4 @@
const updateLeaderboards = require("./update-leaderboards");
const deleteOldLogs = require("./delete-old-logs");
import updateLeaderboards from "./update-leaderboards";
import deleteOldLogs from "./delete-old-logs";
module.exports = [updateLeaderboards, deleteOldLogs];
export default [updateLeaderboards, deleteOldLogs];

View file

@ -1,6 +1,6 @@
const { CronJob } = require("cron");
const BotDAO = require("../dao/bot");
const LeaderboardsDAO = require("../dao/leaderboards");
import { CronJob } from "cron";
import BotDAO from "../dao/bot";
import LeaderboardsDAO from "../dao/leaderboards";
const CRON_SCHEDULE = "30 4/5 * * * *";
const RECENT_AGE_MINUTES = 10;
@ -51,4 +51,4 @@ async function updateLeaderboards() {
await updateLeaderboardAndNotifyChanges("60");
}
module.exports = new CronJob(CRON_SCHEDULE, updateLeaderboards);
export default new CronJob(CRON_SCHEDULE, updateLeaderboards);

View file

@ -1,94 +0,0 @@
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.ctx.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.
* Without this, any errors thrown will not be caught by the error handling middleware, and
* the app will hang!
*/
function asyncHandler(handler) {
return async (req, res, next) => {
try {
const handlerData = await handler(req, res);
if (!res.headersSent) {
if (handlerData) {
res.json(handlerData);
} else {
res.sendStatus(204);
}
}
next();
} catch (error) {
next(error);
}
};
}
function validateRequest(validationSchema) {
/**
* 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 ?? {}),
};
}
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(
500,
validationErrorMessage ??
`${errorMessage} (${error.details[0].context.value})`
);
}
});
next();
};
}
module.exports = {
validateConfiguration,
asyncHandler,
validateRequest,
};

View file

@ -0,0 +1,113 @@
import _ from "lodash";
import joi from "joi";
import MonkeyError from "../handlers/error";
import { Response, NextFunction, RequestHandler } from "express";
import {
handleMonkeyResponse,
MonkeyResponse,
} from "../handlers/monkey-response";
interface ConfigurationValidationOptions {
criteria: (configuration: MonkeyTypes.Configuration) => boolean;
invalidMessage?: string;
}
/**
* This utility checks that the server's configuration matches
* the criteria.
*/
function validateConfiguration(
options: ConfigurationValidationOptions
): RequestHandler {
const { criteria, invalidMessage } = 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."
);
}
next();
};
}
type AsyncHandler = (
req: MonkeyTypes.Request,
res: Response
) => Promise<MonkeyResponse>;
/**
* 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.
* Without this, any errors thrown will not be caught by the error handling middleware, and
* the app will hang!
*/
function asyncHandler(handler: AsyncHandler): RequestHandler {
return async (
req: MonkeyTypes.Request,
res: Response,
next: NextFunction
) => {
try {
const handlerData = await handler(req, res);
return handleMonkeyResponse(handlerData, res);
} catch (error) {
next(error);
}
};
}
interface ValidationSchema {
body?: {};
query?: {};
params?: {};
validationErrorMessage?: string;
}
function validateRequest(validationSchema: ValidationSchema): RequestHandler {
/**
* 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 ?? {}),
};
}
const { validationErrorMessage } = validationSchema;
const normalizedValidationSchema: ValidationSchema = _.omit(
validationSchema,
"validationErrorMessage"
);
return (req: MonkeyTypes.Request, _res: Response, next: NextFunction) => {
_.each(
normalizedValidationSchema,
(schema: {}, key: keyof ValidationSchema) => {
const joiSchema = joi.object().keys(schema);
const { error } = joiSchema.validate(req[key] ?? {});
if (error) {
const errorMessage = error.details[0].message;
throw new MonkeyError(
500,
validationErrorMessage ??
`${errorMessage} (${error.details[0].context.value})`
);
}
}
);
next();
};
}
export { validateConfiguration, asyncHandler, validateRequest };

View file

@ -1,8 +1,9 @@
const MonkeyError = require("../handlers/error");
const { verifyIdToken } = require("../handlers/auth");
import MonkeyError from "../handlers/error";
import { verifyIdToken } from "../handlers/auth";
const DEFAULT_OPTIONS = {
isPublic: false,
acceptMonkeyTokens: false,
};
function authenticateRequest(options = DEFAULT_OPTIONS) {
@ -12,7 +13,7 @@ function authenticateRequest(options = DEFAULT_OPTIONS) {
let token = null;
if (authHeader) {
token = await authenticateWithAuthHeader(authHeader);
token = await authenticateWithAuthHeader(authHeader, options);
} else if (options.isPublic) {
return next();
} else if (process.env.MODE === "dev") {
@ -49,18 +50,21 @@ function authenticateWithBody(body) {
};
}
async function authenticateWithAuthHeader(authHeader) {
async function authenticateWithAuthHeader(authHeader, options) {
const token = authHeader.split(" ");
const authScheme = token[0].trim();
const credentials = token[1];
if (authScheme === "Bearer") {
return await authenticateWithBearerToken(credentials);
switch (authScheme) {
case "Bearer":
return await authenticateWithBearerToken(credentials);
case "MonkeyToken":
return await authenticateWithMonkeyToken(credentials, options);
}
throw new MonkeyError(
400,
401,
"Unknown authentication scheme",
`The authentication scheme "${authScheme}" is not implemented.`
);
@ -92,6 +96,12 @@ async function authenticateWithBearerToken(token) {
}
}
module.exports = {
authenticateRequest,
};
async function authenticateWithMonkeyToken(token, options) {
if (!options.acceptMonkeyTokens) {
throw new MonkeyError(401, "This endpoint does not accept MonkeyTokens.");
}
throw new MonkeyError(401, "MonkeyTokens are not implemented.");
}
export { authenticateRequest };

View file

@ -1,6 +1,6 @@
const ConfigurationDAO = require("../dao/configuration");
import ConfigurationDAO from "../dao/configuration";
async function contextMiddleware(req, res, next) {
async function contextMiddleware(req, _res, next) {
const configuration = await ConfigurationDAO.getCachedConfiguration(true);
req.ctx = {
@ -13,4 +13,4 @@ async function contextMiddleware(req, res, next) {
next();
}
module.exports = contextMiddleware;
export default contextMiddleware;

View file

@ -0,0 +1,58 @@
import db from "../init/db";
import { v4 as uuidv4 } from "uuid";
import Logger from "../handlers/logger";
import MonkeyError from "../handlers/error";
import {
MonkeyResponse,
handleMonkeyResponse,
} from "../handlers/monkey-response";
async function errorHandlingMiddleware(error, req, res, _next) {
const monkeyResponse = new MonkeyResponse();
monkeyResponse.status = 500;
monkeyResponse.data = {
errorId: error.errorId ?? uuidv4(),
uid: error.uid ?? req.ctx?.decodedToken?.uid,
};
if (error instanceof MonkeyError) {
monkeyResponse.message = error.message;
monkeyResponse.status = error.status;
} else if (/ECONNREFUSED.*27017/i.test(error.message)) {
monkeyResponse.message =
"Could not connect to the database. It may be down.";
} else {
monkeyResponse.message =
"Oops! Our monkeys dropped their bananas. Please try again later.";
}
if (process.env.MODE !== "dev" && monkeyResponse.status > 400) {
const { uid, errorId } = monkeyResponse.data;
try {
await Logger.log(
"system_error",
`${monkeyResponse.status} ${error.message} ${error.stack}`,
uid
);
await db.collection("errors").insertOne({
_id: errorId,
timestamp: Date.now(),
status: monkeyResponse.status,
uid,
message: error.message,
stack: error.stack,
endpoint: req.originalUrl,
});
} catch (e) {
console.error("Failed to save error.");
console.error(e);
}
} else {
console.error(error.message);
}
return handleMonkeyResponse(monkeyResponse, res);
}
export default errorHandlingMiddleware;

View file

@ -1,263 +0,0 @@
const rateLimit = require("express-rate-limit");
const getAddress = (req) =>
req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.ip ||
"255.255.255.255";
const message = "Too many requests, please try again later";
const multiplier = process.env.MODE === "dev" ? 100 : 1;
// Config Routing
exports.configUpdate = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 500 * multiplier,
message,
keyGenerator: getAddress,
});
exports.configGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 120 * multiplier,
message,
keyGenerator: getAddress,
});
// Leaderboards Routing
exports.leaderboardsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
// New Quotes Routing
exports.newQuotesGet = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * multiplier,
message,
keyGenerator: getAddress,
});
exports.newQuotesAdd = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.newQuotesAction = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * multiplier,
message,
keyGenerator: getAddress,
});
// Quote Ratings Routing
exports.quoteRatingsGet = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * multiplier,
message,
keyGenerator: getAddress,
});
exports.quoteRatingsSubmit = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * multiplier,
message,
keyGenerator: getAddress,
});
// Quote reporting
exports.quoteReportSubmit = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * multiplier,
message,
keyGenerator: getAddress,
});
// Presets Routing
exports.presetsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.presetsAdd = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.presetsRemove = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.presetsEdit = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
// PSA (Public Service Announcement) Routing
exports.psaGet = rateLimit({
windowMs: 60 * 1000,
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
// Results Routing
exports.resultsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.resultsAdd = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * multiplier,
message,
keyGenerator: getAddress,
});
exports.resultsTagsUpdate = rateLimit({
windowMs: 60 * 60 * 1000,
max: 30 * multiplier,
message,
keyGenerator: getAddress,
});
exports.resultsDeleteAll = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 10 * multiplier,
message,
keyGenerator: getAddress,
});
exports.resultsLeaderboardGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.resultsLeaderboardQualificationGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
// Users Routing
exports.userGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userSignup = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 1 day
max: 3 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userDelete = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 1 day
max: 3 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userCheckName = rateLimit({
windowMs: 60 * 1000,
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userUpdateName = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 1 day
max: 3 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userUpdateLBMemory = rateLimit({
windowMs: 60 * 1000,
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userUpdateEmail = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userClearPB = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userTagsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userTagsRemove = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 30 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userTagsClearPB = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userTagsEdit = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 30 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userTagsAdd = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 30 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userDiscordLink = exports.usersTagsEdit = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 15 * multiplier,
message,
keyGenerator: getAddress,
});
exports.userDiscordUnlink = exports.usersTagsEdit = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 15 * multiplier,
message,
keyGenerator: getAddress,
});

View file

@ -0,0 +1,277 @@
import { Response, NextFunction } from "express";
import rateLimit, { Options } from "express-rate-limit";
import MonkeyError from "../handlers/error";
const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 100 : 1;
const getAddress = (req: MonkeyTypes.Request, _res: Response): string => {
return (req.headers["cf-connecting-ip"] ||
req.headers["x-forwarded-for"] ||
req.ip ||
"255.255.255.255") as string;
};
const customHandler = (
_req: MonkeyTypes.Request,
_res: Response,
_next: NextFunction,
_options: Options
): void => {
throw new MonkeyError(429, "Too many attempts, please try again later.");
};
// Config Routing
export const configUpdate = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const configGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 120 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Leaderboards Routing
export const leaderboardsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// New Quotes Routing
export const newQuotesGet = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const newQuotesAdd = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const newQuotesAction = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Quote Ratings Routing
export const quoteRatingsGet = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const quoteRatingsSubmit = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Quote reporting
export const quoteReportSubmit = rateLimit({
windowMs: 30 * 60 * 1000, // 30 min
max: 50 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Presets Routing
export const presetsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const presetsAdd = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const presetsRemove = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const presetsEdit = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// PSA (Public Service Announcement) Routing
export const psaGet = rateLimit({
windowMs: 60 * 1000,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Results Routing
export const resultsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const resultsAdd = rateLimit({
windowMs: 60 * 60 * 1000,
max: 500 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const resultsTagsUpdate = rateLimit({
windowMs: 60 * 60 * 1000,
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const resultsDeleteAll = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 10 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const resultsLeaderboardGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const resultsLeaderboardQualificationGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
// Users Routing
export const userGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userSignup = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 1 day
max: 3 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userDelete = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 1 day
max: 3 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userCheckName = rateLimit({
windowMs: 60 * 1000,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userUpdateName = rateLimit({
windowMs: 24 * 60 * 60 * 1000, // 1 day
max: 3 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userUpdateLBMemory = rateLimit({
windowMs: 60 * 1000,
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userUpdateEmail = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userClearPB = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userTagsGet = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userTagsRemove = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userTagsClearPB = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 60 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userTagsEdit = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userTagsAdd = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 30 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const userDiscordLink = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 15 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});
export const usersTagsEdit = userDiscordLink;
export const userDiscordUnlink = rateLimit({
windowMs: 60 * 60 * 1000, // 60 min
max: 15 * REQUEST_MULTIPLIER,
keyGenerator: getAddress,
handler: customHandler,
});

View file

@ -13,7 +13,7 @@
"cron": "1.8.2",
"dotenv": "10.0.0",
"express": "4.17.1",
"express-rate-limit": "5.3.0",
"express-rate-limit": "6.2.1",
"firebase-admin": "9.11.0",
"helmet": "4.6.0",
"joi": "17.6.0",
@ -28,6 +28,11 @@
"ua-parser-js": "0.7.28",
"uuid": "8.3.2"
},
"devDependencies": {
"@types/cors": "2.8.12",
"@types/node": "17.0.18",
"@types/uuid": "8.3.4"
},
"engines": {
"node": "16.13.2",
"npm": "8.1.2"
@ -422,6 +427,12 @@
"@types/node": "*"
}
},
"node_modules/@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"node_modules/@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@ -472,9 +483,9 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"node_modules/@types/node": {
"version": "17.0.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz",
"integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA=="
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA=="
},
"node_modules/@types/qs": {
"version": "6.9.7",
@ -495,6 +506,12 @@
"@types/node": "*"
}
},
"node_modules/@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"node_modules/abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -1258,9 +1275,15 @@
}
},
"node_modules/express-rate-limit": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz",
"integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew=="
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.2.1.tgz",
"integrity": "sha512-22ovnpEiKR5iAMXDOQ7A6aOvb078JLvoHGlyrrWBl3PeJ34coyakaviPelj4Nc8d+yDoVIWYmaUNP5aYT4ICDQ==",
"engines": {
"node": ">= 14.5.0"
},
"peerDependencies": {
"express": "^4"
}
},
"node_modules/extend": {
"version": "3.0.2",
@ -3779,6 +3802,12 @@
"@types/node": "*"
}
},
"@types/cors": {
"version": "2.8.12",
"resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.12.tgz",
"integrity": "sha512-vt+kDhq/M2ayberEtJcIN/hxXy1Pk+59g2FV/ZQceeaTyCtCucjL2Q7FXlFjtWn4n15KCr1NE2lNNFhp0lEThw==",
"dev": true
},
"@types/express": {
"version": "4.17.13",
"resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.13.tgz",
@ -3829,9 +3858,9 @@
"integrity": "sha512-YATxVxgRqNH6nHEIsvg6k2Boc1JHI9ZbH5iWFFv/MTkchz3b1ieGDa5T0a9RznNdI0KhVbdbWSN+KWWrQZRxTw=="
},
"@types/node": {
"version": "17.0.16",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.16.tgz",
"integrity": "sha512-ydLaGVfQOQ6hI1xK2A5nVh8bl0OGoIfYMxPWHqqYe9bTkWCfqiVvZoh2I/QF2sNSkZzZyROBoTefIEI+PB6iIA=="
"version": "17.0.18",
"resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.18.tgz",
"integrity": "sha512-eKj4f/BsN/qcculZiRSujogjvp5O/k4lOW5m35NopjZM/QwLOR075a8pJW5hD+Rtdm2DaCVPENS6KtSQnUD6BA=="
},
"@types/qs": {
"version": "6.9.7",
@ -3852,6 +3881,12 @@
"@types/node": "*"
}
},
"@types/uuid": {
"version": "8.3.4",
"resolved": "https://registry.npmjs.org/@types/uuid/-/uuid-8.3.4.tgz",
"integrity": "sha512-c/I8ZRb51j+pYGAu5CrFMRxqZ2ke4y2grEBO5AUjgSkSk+qT2Ea+OdWElz/OiMf5MNpn2b17kuVBwZLQJXzihw==",
"dev": true
},
"abbrev": {
"version": "1.1.1",
"resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz",
@ -4449,9 +4484,10 @@
}
},
"express-rate-limit": {
"version": "5.3.0",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-5.3.0.tgz",
"integrity": "sha512-qJhfEgCnmteSeZAeuOKQ2WEIFTX5ajrzE0xS6gCOBCoRQcU+xEzQmgYQQTpzCcqUAAzTEtu4YEih4pnLfvNtew=="
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-6.2.1.tgz",
"integrity": "sha512-22ovnpEiKR5iAMXDOQ7A6aOvb078JLvoHGlyrrWBl3PeJ34coyakaviPelj4Nc8d+yDoVIWYmaUNP5aYT4ICDQ==",
"requires": {}
},
"extend": {
"version": "3.0.2",

View file

@ -4,7 +4,12 @@
"license": "GPL-3.0",
"private": true,
"scripts": {
"start:dev": "nodemon --watch ./ ./server.js"
"build": "tsc --build",
"watch": "tsc --build --watch",
"clean": "tsc --build --clean",
"start": "npm run build && node ./build/server.js",
"dev": "nodemon --watch build/server.js ./build/server.js",
"start:dev": "concurrently \"npm run watch\" \"npm run dev\""
},
"engines": {
"node": "16.13.2",
@ -15,7 +20,7 @@
"cron": "1.8.2",
"dotenv": "10.0.0",
"express": "4.17.1",
"express-rate-limit": "5.3.0",
"express-rate-limit": "6.2.1",
"firebase-admin": "9.11.0",
"helmet": "4.6.0",
"joi": "17.6.0",
@ -29,5 +34,10 @@
"string-similarity": "4.0.4",
"ua-parser-js": "0.7.28",
"uuid": "8.3.2"
},
"devDependencies": {
"@types/cors": "2.8.12",
"@types/node": "17.0.18",
"@types/uuid": "8.3.4"
}
}

View file

@ -1,94 +0,0 @@
const express = require("express");
const { config } = require("dotenv");
const path = require("path");
const MonkeyError = require("./handlers/error");
config({ path: path.join(__dirname, ".env") });
const cors = require("cors");
const admin = require("firebase-admin");
const Logger = require("./handlers/logger.js");
// eslint-disable-next-line
const serviceAccount = require("./credentials/serviceAccountKey.json");
const db = require("./init/db");
const jobs = require("./jobs");
const addApiRoutes = require("./api/routes");
const contextMiddleware = require("./middlewares/context");
const ConfigurationDAO = require("./dao/configuration");
const PORT = process.env.PORT || 5005;
// MIDDLEWARE & SETUP
const app = express();
app.use(express.urlencoded({ extended: true }));
app.use(express.json());
app.use(cors());
app.set("trust proxy", 1);
app.use(contextMiddleware);
app.use((req, res, next) => {
if (process.env.MAINTENANCE === "true" || req.ctx.configuration.maintenance) {
res.status(503).json({ message: "Server is down for maintenance" });
} else {
next();
}
});
addApiRoutes(app);
//DO NOT REMOVE NEXT, EVERYTHING WILL EXPLODE
app.use(function (e, req, res, _next) {
if (/ECONNREFUSED.*27017/i.test(e.message)) {
e.message = "Could not connect to the database. It may have crashed.";
delete e.stack;
}
let monkeyError;
if (e.errorID) {
//its a monkey error
monkeyError = e;
} else {
//its a server error
monkeyError = new MonkeyError(e.status, e.message, e.stack);
}
if (!monkeyError.uid && req.ctx?.decodedToken) {
monkeyError.uid = req.ctx.decodedToken.uid;
}
if (process.env.MODE !== "dev" && monkeyError.status > 400) {
Logger.log(
"system_error",
`${monkeyError.status} ${monkeyError.message} ${monkeyError.stack}`,
monkeyError.uid
);
db.collection("errors").insertOne({
_id: monkeyError.errorID,
timestamp: Date.now(),
status: monkeyError.status,
uid: monkeyError.uid,
message: monkeyError.message,
stack: monkeyError.stack,
});
monkeyError.stack = undefined;
} else {
console.error(monkeyError.message);
}
return res.status(monkeyError.status || 500).json(monkeyError);
});
console.log("Starting server...");
app.listen(PORT, async () => {
console.log(`Listening on port ${PORT}`);
console.log("Connecting to database...");
await db.connect();
console.log("Database connected");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
});
await ConfigurationDAO.getLiveConfiguration();
console.log("Starting cron jobs...");
jobs.forEach((job) => job.start());
});

43
backend/server.ts Normal file
View file

@ -0,0 +1,43 @@
import "dotenv/config";
import admin, { ServiceAccount } from "firebase-admin";
import serviceAccount from "./credentials/serviceAccountKey.json";
import db from "./init/db.js";
import jobs from "./jobs/index.js";
import ConfigurationDAO from "./dao/configuration.js";
import app from "./app";
async function bootServer(port) {
try {
console.log("Connecting to database...");
await db.connect();
console.log("Connected to database");
console.log("Initializing Firebase app instance...");
admin.initializeApp({
credential: admin.credential.cert(
serviceAccount as unknown as ServiceAccount
)
});
console.log("Firebase app initialized");
console.log("Fetching live configuration...");
await ConfigurationDAO.getLiveConfiguration();
console.log("Live configuration fetched");
console.log("Starting cron jobs...");
jobs.forEach((job) => job.start());
console.log("Cron jobs started");
} catch (error) {
console.error("Failed to boot server");
console.error(error);
return process.exit(1);
}
return app.listen(PORT, () => {
console.log(`API server listening on port ${port}`);
});
}
const PORT = process.env.PORT || 5005;
bootServer(PORT);

19
backend/tsconfig.json Normal file
View file

@ -0,0 +1,19 @@
{
"compilerOptions": {
"incremental": true,
"module": "commonjs",
"target": "es6",
"sourceMap": false,
"allowJs": true,
"checkJs": true,
"outDir": "build",
"moduleResolution": "node",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"paths": {
"anticheat": ["./anticheat/anticheat"]
}
},
"exclude": ["node_modules", "build"]
}

5
backend/types/anticheat.d.ts vendored Normal file
View file

@ -0,0 +1,5 @@
declare module "anticheat" {
function validateResult(result): string;
function validateKeys(result, uid): string;
}

32
backend/types/types.d.ts vendored Normal file
View file

@ -0,0 +1,32 @@
type ExpressRequest = import("express").Request;
declare namespace MonkeyTypes {
interface Configuration {
maintenance: boolean;
quoteReport: {
enabled: boolean;
maxReports: number;
contentReportLimit: number;
};
quoteSubmit: {
enabled: boolean;
};
resultObjectHashCheck: {
enabled: boolean;
};
monkeyTokens: {
enabled: boolean;
};
}
interface Context {
configuration: Configuration;
decodedToken: {
uid: string | null;
};
}
interface Request extends ExpressRequest {
ctx: Context;
}
}

View file

@ -1,16 +1,17 @@
const { config } = require("dotenv");
const path = require("path");
config({ path: path.join(__dirname, ".env") });
import path from "path";
import serviceAccount from "./credentials/serviceAccountKey.json";
import admin, { ServiceAccount } from "firebase-admin";
import db from "./init/db";
import { config } from "dotenv";
const db = require("./init/db");
const admin = require("firebase-admin");
// eslint-disable-next-line
const serviceAccount = require("./credentials/serviceAccountKey.json");
config({ path: path.join(__dirname, ".env") });
async function main() {
await db.connect();
await admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
credential: admin.credential.cert(
(serviceAccount as unknown) as ServiceAccount
),
});
console.log("Database Connected!!");
refactor();

View file

@ -0,0 +1,18 @@
const BASE_PATH = "/configs";
export default function getConfigsEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["configs"] {
async function get(): Ape.EndpointData {
return await apeClient.get(BASE_PATH);
}
async function save(config: MonkeyTypes.Config): Ape.EndpointData {
return await apeClient.patch(BASE_PATH, { payload: { config } });
}
return {
get,
save,
};
}

View file

@ -0,0 +1,17 @@
import getConfigsEndpoints from "./configs";
import getLeaderboardsEndpoints from "./leaderboards";
import getPresetsEndpoints from "./presets";
import getPsasEndpoints from "./psas";
import getQuotesEndpoints from "./quotes";
import getResultsEndpoints from "./results";
import getUsersEndpoints from "./users";
export default {
getConfigsEndpoints,
getLeaderboardsEndpoints,
getPresetsEndpoints,
getPsasEndpoints,
getQuotesEndpoints,
getResultsEndpoints,
getUsersEndpoints,
};

View file

@ -0,0 +1,39 @@
const BASE_PATH = "/leaderboards";
export default function getLeaderboardsEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["leaderboards"] {
async function get(
language: string,
mode: MonkeyTypes.Mode,
mode2: string | number,
skip = 0,
limit = 50
): Ape.EndpointData {
const searchQuery = {
language,
mode,
mode2,
skip,
limit: Math.max(Math.min(limit, 50), 0),
};
return await apeClient.get(BASE_PATH, { searchQuery });
}
async function getRank(
language: string,
mode: MonkeyTypes.Mode,
mode2: string | number
): Ape.EndpointData {
const searchQuery = {
language,
mode,
mode2,
};
return await apeClient.get(`${BASE_PATH}/rank`, { searchQuery });
}
return { get, getRank };
}

View file

@ -0,0 +1,41 @@
const BASE_PATH = "/presets";
export default function getPresetsEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["presets"] {
async function get(): Ape.EndpointData {
return await apeClient.get(BASE_PATH);
}
async function add(
presetName: string,
configChanges: MonkeyTypes.ConfigChanges
): Ape.EndpointData {
const payload = {
name: presetName,
config: configChanges,
};
return await apeClient.post(BASE_PATH, { payload });
}
async function edit(
presetId: string,
presetName: string,
configChanges: MonkeyTypes.ConfigChanges
): Ape.EndpointData {
const payload = {
_id: presetId,
name: presetName,
config: configChanges,
};
return await apeClient.patch(BASE_PATH, { payload });
}
async function _delete(presetId: string): Ape.EndpointData {
return await apeClient.delete(`${BASE_PATH}/${presetId}`);
}
return { get, add, edit, delete: _delete };
}

View file

@ -0,0 +1,11 @@
const BASE_PATH = "/psas";
export default function getPsasEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["psas"] {
async function get(): Ape.EndpointData {
return await apeClient.get(BASE_PATH);
}
return { get };
}

View file

@ -0,0 +1,95 @@
const BASE_PATH = "/quotes";
export default function getQuotesEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["quotes"] {
async function get(): Ape.EndpointData {
return await apeClient.get(BASE_PATH);
}
async function submit(
text: string,
source: string,
language: string,
captcha: string
): Ape.EndpointData {
const payload = {
text,
source,
language,
captcha,
};
return await apeClient.post(BASE_PATH, { payload });
}
async function approveSubmission(
quoteSubmissionId: string,
editText?: string,
editSource?: string
): Ape.EndpointData {
const payload = {
quoteId: quoteSubmissionId,
editText,
editSource,
};
return await apeClient.post(`${BASE_PATH}/approve`, { payload });
}
async function rejectSubmission(quoteSubmissionId: string): Ape.EndpointData {
return await apeClient.post(`${BASE_PATH}/reject`, {
payload: { quoteId: quoteSubmissionId },
});
}
async function getRating(quote: MonkeyTypes.Quote): Ape.EndpointData {
const searchQuery = {
quoteId: quote.id,
language: quote.language,
};
return await apeClient.get(`${BASE_PATH}/rating`, { searchQuery });
}
async function addRating(
quote: MonkeyTypes.Quote,
rating: number
): Ape.EndpointData {
const payload = {
quoteId: quote.id,
rating,
language: quote.language,
};
return await apeClient.post(`${BASE_PATH}/rating`, { payload });
}
async function report(
quoteId: string,
quoteLanguage: string,
reason: string,
comment: string,
captcha: string
): Ape.EndpointData {
const payload = {
quoteId,
quoteLanguage,
reason,
comment,
captcha,
};
return await apeClient.post(`${BASE_PATH}/report`, { payload });
}
return {
get,
submit,
approveSubmission,
rejectSubmission,
getRating,
addRating,
report,
};
}

View file

@ -0,0 +1,30 @@
const BASE_PATH = "/results";
export default function getResultsEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["results"] {
async function get(): Ape.EndpointData {
return await apeClient.get(BASE_PATH);
}
async function save(
result: MonkeyTypes.Result<MonkeyTypes.Mode>
): Ape.EndpointData {
return await apeClient.post(BASE_PATH, { payload: { result } });
}
async function updateTags(
resultId: string,
tagIds: string[]
): Ape.EndpointData {
return await apeClient.patch(`${BASE_PATH}/tags`, {
payload: { resultId, tagIds },
});
}
async function deleteAll(): Ape.EndpointData {
return await apeClient.delete(BASE_PATH);
}
return { get, save, updateTags, deleteAll };
}

View file

@ -0,0 +1,124 @@
const BASE_PATH = "/users";
export default function getUsersEndpoints(
apeClient: Ape.Client
): Ape.Endpoints["users"] {
async function getData(): Ape.EndpointData {
return await apeClient.get(BASE_PATH);
}
async function create(
name: string,
email?: string,
uid?: string
): Ape.EndpointData {
const payload = {
email,
name,
uid,
};
return await apeClient.post(`${BASE_PATH}/signup`, { payload });
}
async function getNameAvailability(name: string): Ape.EndpointData {
return await apeClient.get(`${BASE_PATH}/checkName/${name}`);
}
async function _delete(): Ape.EndpointData {
return await apeClient.delete(BASE_PATH);
}
async function updateName(name: string): Ape.EndpointData {
return await apeClient.patch(`${BASE_PATH}/name`, { payload: { name } });
}
async function updateLeaderboardMemory(
mode: string,
mode2: MonkeyTypes.Mode2<any>,
language: string,
rank: number
): Ape.EndpointData {
const payload = {
mode,
mode2,
language,
rank,
};
return await apeClient.patch(`${BASE_PATH}/leaderboardMemory`, { payload });
}
async function updateEmail(
newEmail: string,
previousEmail: string
): Ape.EndpointData {
const payload = {
newEmail,
previousEmail,
};
return await apeClient.patch(`${BASE_PATH}/email`, { payload });
}
async function deletePersonalBests(): Ape.EndpointData {
return await apeClient.delete(`${BASE_PATH}/personalBests`);
}
async function getTags(): Ape.EndpointData {
return await apeClient.get(`${BASE_PATH}/tags`);
}
async function createTag(tagName: string): Ape.EndpointData {
return await apeClient.post(`${BASE_PATH}/tags`, { payload: { tagName } });
}
async function editTag(tagId: string, newName: string): Ape.EndpointData {
const payload = {
tagId,
newName,
};
return await apeClient.patch(`${BASE_PATH}/tags`, { payload });
}
async function deleteTag(tagId: string): Ape.EndpointData {
return await apeClient.delete(`${BASE_PATH}/tags/${tagId}`);
}
async function deleteTagPersonalBest(tagId: string): Ape.EndpointData {
return await apeClient.delete(`${BASE_PATH}/tags/${tagId}/personalBest`);
}
async function linkDiscord(data: {
tokenType: string;
accessToken: string;
uid?: string;
}): Ape.EndpointData {
return await apeClient.post(`${BASE_PATH}/discord/link`, {
payload: { data },
});
}
async function unlinkDiscord(): Ape.EndpointData {
return await apeClient.post(`${BASE_PATH}/discord/unlink`);
}
return {
getData,
create,
getNameAvailability,
delete: _delete,
updateName,
updateLeaderboardMemory,
updateEmail,
deletePersonalBests,
getTags,
createTag,
editTag,
deleteTag,
deleteTagPersonalBest,
linkDiscord,
unlinkDiscord,
};
}

View file

@ -0,0 +1,123 @@
import axios, { AxiosRequestConfig, AxiosResponse } from "axios";
import endpoints from "./endpoints";
const DEV_SERVER_HOST = "http://localhost:5005";
const PROD_SERVER_HOST = "https://api.monkeytype.com";
const API_PATH = "";
const BASE_URL =
window.location.hostname === "localhost" ? DEV_SERVER_HOST : PROD_SERVER_HOST;
const API_URL = `${BASE_URL}${API_PATH}`;
// Adapts the ape client's view of request options to the underlying HTTP client.
async function adaptRequestOptions(
options: Ape.RequestOptions
): Promise<AxiosRequestConfig> {
const currentUser = firebase.auth().currentUser;
const idToken = currentUser && (await currentUser.getIdToken());
return {
params: options.searchQuery,
data: options.payload,
headers: {
Accept: "application/json",
"Content-Type": "application/json",
...(idToken && { Authorization: `Bearer ${idToken}` }),
},
};
}
type AxiosClientMethod = (
endpoint: string,
config: AxiosRequestConfig
) => Promise<AxiosResponse>;
type AxiosClientDataMethod = (
endpoint: string,
data: any,
config: AxiosRequestConfig
) => Promise<AxiosResponse>;
type AxiosClientMethods = AxiosClientMethod & AxiosClientDataMethod;
// Wrap the underlying HTTP client's method with our own.
function apeifyClientMethod(
clientMethod: AxiosClientMethods,
methodType: Ape.MethodTypes
): Ape.ClientMethod {
return async (
endpoint: string,
options: Ape.RequestOptions = {}
): Ape.EndpointData => {
let errorMessage = "";
try {
const requestOptions: AxiosRequestConfig = await adaptRequestOptions(
options
);
let response;
if (methodType === "get" || methodType === "delete") {
response = await clientMethod(endpoint, requestOptions);
} else {
response = await clientMethod(
endpoint,
requestOptions.data,
requestOptions
);
}
const { message, data } = response.data as Ape.ApiResponse;
return {
status: response.status,
message,
data,
};
} catch (error) {
console.error(error);
const typedError = error as Error;
errorMessage = typedError.message;
if (axios.isAxiosError(typedError)) {
return {
status: typedError.response?.status ?? 500,
message: typedError.message,
...typedError.response?.data,
};
}
}
return {
status: 500,
message: errorMessage,
};
};
}
const axiosClient = axios.create({
baseURL: API_URL,
timeout: 10000,
});
const apeClient: Ape.Client = {
get: apeifyClientMethod(axiosClient.get, "get"),
post: apeifyClientMethod(axiosClient.post, "post"),
put: apeifyClientMethod(axiosClient.put, "put"),
patch: apeifyClientMethod(axiosClient.patch, "patch"),
delete: apeifyClientMethod(axiosClient.delete, "delete"),
};
// API Endpoints
const Ape: Ape.Endpoints = {
users: endpoints.getUsersEndpoints(apeClient),
configs: endpoints.getConfigsEndpoints(apeClient),
results: endpoints.getResultsEndpoints(apeClient),
psas: endpoints.getPsasEndpoints(apeClient),
quotes: endpoints.getQuotesEndpoints(apeClient),
leaderboards: endpoints.getLeaderboardsEndpoints(apeClient),
presets: endpoints.getPresetsEndpoints(apeClient),
};
export default Ape;

134
frontend/src/scripts/ape/types/ape.d.ts vendored Normal file
View file

@ -0,0 +1,134 @@
declare namespace Ape {
type ClientMethod = (
endpoint: string,
config?: RequestOptions
) => Promise<Response>;
interface ApiResponse {
message: string;
data: any | null;
}
interface Client {
get: ClientMethod;
post: ClientMethod;
put: ClientMethod;
patch: ClientMethod;
delete: ClientMethod;
}
type MethodTypes = keyof Client;
interface RequestOptions {
searchQuery?: Record<string, any>;
payload?: any;
}
interface Response {
status: number;
message: string;
data?: any;
}
type EndpointData = Promise<Response>;
type Endpoint = () => EndpointData;
interface Endpoints {
configs: {
get: Endpoint;
save: (config: MonkeyTypes.Config) => EndpointData;
};
leaderboards: {
get: (
language: string,
mode: MonkeyTypes.Mode,
mode2: string | number,
skip: number,
limit?: number
) => EndpointData;
getRank: (
language: string,
mode: MonkeyTypes.Mode,
mode2: string | number
) => EndpointData;
};
presets: {
get: Endpoint;
add: (
presetName: string,
configChanges: MonkeyTypes.ConfigChanges
) => EndpointData;
edit: (
presetId: string,
presetName: string,
configChanges: MonkeyTypes.ConfigChanges
) => EndpointData;
delete: (presetId: string) => EndpointData;
};
psas: {
get: Endpoint;
};
quotes: {
get: Endpoint;
submit: (
text: string,
source: string,
language: string,
captcha: string
) => EndpointData;
approveSubmission: (
quoteSubmissionId: string,
editText?: string,
editSource?: string
) => EndpointData;
rejectSubmission: (quoteSubmissionId: string) => EndpointData;
getRating: (quote: MonkeyTypes.Quote) => EndpointData;
addRating: (quote: MonkeyTypes.Quote, rating: number) => EndpointData;
report: (
quoteId: string,
quoteLanguage: string,
reason: string,
comment: string,
captcha: string
) => EndpointData;
};
users: {
getData: Endpoint;
create: (name: string, email?: string, uid?: string) => EndpointData;
getNameAvailability: (name: string) => EndpointData;
delete: Endpoint;
updateName: (name: string) => EndpointData;
updateLeaderboardMemory: (
mode: string,
mode2: MonkeyTypes.Mode2<any>,
language: string,
rank: number
) => EndpointData;
updateEmail: (newEmail: string, previousEmail: string) => EndpointData;
deletePersonalBests: Endpoint;
getTags: Endpoint;
createTag: (tagName: string) => EndpointData;
editTag: (tagId: string, newName: string) => EndpointData;
deleteTag: (tagId: string) => EndpointData;
deleteTagPersonalBest: (tagId: string) => EndpointData;
linkDiscord: (data: {
tokenType: string;
accessToken: string;
uid?: string;
}) => EndpointData;
unlinkDiscord: Endpoint;
};
results: {
get: Endpoint;
save: (result: MonkeyTypes.Result) => EndpointData;
updateTags: (resultId: string, tagIds: string[]) => EndpointData;
deleteAll: Endpoint;
};
}
}

View file

@ -1,60 +0,0 @@
import axios from "axios";
const apiPath = "";
let baseURL;
if (window.location.hostname === "localhost") {
baseURL = "http://localhost:5005" + apiPath;
} else {
baseURL = "https://api.monkeytype.com" + apiPath;
}
const axiosInstance = axios.create({
baseURL: baseURL,
timeout: 10000,
});
// Request interceptor for API calls
axiosInstance.interceptors.request.use(
async (config) => {
let idToken: string | null;
if (firebase.auth().currentUser != null) {
idToken = await firebase.auth().currentUser.getIdToken();
} else {
idToken = null;
}
if (idToken) {
config.headers = {
Authorization: `Bearer ${idToken}`,
Accept: "application/json",
"Content-Type": "application/json",
};
} else {
config.headers = {
Accept: "application/json",
"Content-Type": "application/json",
};
}
return config;
},
(error) => {
Promise.reject(error);
}
);
axiosInstance.interceptors.response.use(
(response) => response,
(error) => {
// whatever you want to do with the error
// console.log('interctepted');
// if(error.response.data.message){
// Notifications.add(`${error.response.data.message}`);
// }else{
// Notifications.add(`${error.response.status} ${error.response.statusText}`);
// }
// return error.response;
throw error;
}
);
export default axiosInstance;

View file

@ -1,3 +1,4 @@
import Ape from "../ape";
import * as Notifications from "../elements/notifications";
import Config, * as UpdateConfig from "../config";
import * as AccountButton from "../elements/account-button";
@ -9,7 +10,6 @@ import * as AllTimeStats from "../account/all-time-stats";
import * as DB from "../db";
import * as TestLogic from "../test/test-logic";
import * as PageController from "../controllers/page-controller";
import axiosInstance from "../axios-instance";
import * as PSA from "../elements/psa";
import * as Focus from "../test/focus";
import * as Loader from "../elements/loader";
@ -44,7 +44,7 @@ export function sendVerificationEmail() {
export async function getDataAndInit() {
try {
console.log("getting account data");
if (ActivePage.get() == "loading") {
if (ActivePage.get() === "loading") {
LoadingPage.updateBar(90);
} else {
LoadingPage.updateBar(45);
@ -70,7 +70,7 @@ export async function getDataAndInit() {
Notifications.add("Failed to get user data: " + msg, -1);
$("#top #menu .account").css("opacity", 1);
if (ActivePage.get() == "loading") PageController.change("");
if (ActivePage.get() === "loading") PageController.change("");
return false;
}
if (ActivePage.get() == "loading") {
@ -98,46 +98,41 @@ export async function getDataAndInit() {
});
let user = firebase.auth().currentUser;
if (snapshot.name == undefined) {
if (!snapshot.name) {
//verify username
if (Misc.isUsernameValid(user.name)) {
//valid, just update
snapshot.name = user.name;
DB.setSnapshot(snapshot);
DB.updateName(user.uid, user.name);
await Ape.users.updateName(user.name);
} else {
//invalid, get new
let nameGood = false;
let name = "";
while (nameGood === false) {
name = await prompt(
while (!nameGood) {
name = prompt(
"Please provide a new username (cannot be longer than 16 characters, can only contain letters, numbers, underscores, dots and dashes):"
);
if (name == null) {
if (!name) {
return false;
}
let response;
try {
response = await axiosInstance.patch("/user/name", { name });
} catch (e) {
let msg = e?.response?.data?.message ?? e.message;
if (e.response.status >= 500) {
Notifications.add("Failed to update name: " + msg, -1);
throw e;
} else {
alert(msg);
}
}
if (response?.status == 200) {
nameGood = true;
Notifications.add("Name updated", 1);
snapshot.name = name;
DB.setSnapshot(snapshot);
$("#menu .icon-button.account .text").text(name);
const response = await Ape.users.updateName(name);
if (response.status !== 200) {
return Notifications.add(
"Failed to update name: " + response.message,
-1
);
}
nameGood = true;
Notifications.add("Name updated", 1);
snapshot.name = name;
DB.setSnapshot(snapshot);
$("#menu .icon-button.account .text").text(name);
}
}
}
@ -339,25 +334,21 @@ export function signIn() {
PageController.change("account");
if (TestLogic.notSignedInLastResult !== null) {
TestLogic.setNotSignedInUid(e.user.uid);
let response;
try {
response = await axiosInstance.post("/results/add", {
result: TestLogic.notSignedInLastResult,
});
} catch (e) {
let msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to save last result: " + msg, -1);
return;
}
const response = await Ape.results.save(
TestLogic.notSignedInLastResult
);
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
TestLogic.clearNotSignedInResult();
Notifications.add("Last test result saved", 1);
return Notifications.add(
"Failed to save last result: " + response.message,
-1
);
}
// PageController.change("account");
TestLogic.clearNotSignedInResult();
Notifications.add("Last test result saved", 1);
}
// PageController.change("test");
//TODO: redirect user to relevant page
})
.catch(function (error) {
@ -392,45 +383,40 @@ export async function signInWithGoogle() {
let nameGood = false;
let name = "";
while (nameGood === false) {
name = await prompt(
while (!nameGood) {
name = prompt(
"Please provide a new username (cannot be longer than 16 characters, can only contain letters, numbers, underscores, dots and dashes):"
);
if (name == null) {
if (!name) {
signOut();
$(".pageLogin .preloader").addClass("hidden");
return;
}
let response;
try {
response = await axiosInstance.get(`/user/checkName/${name}`);
} catch (e) {
let msg = e?.response?.data?.message ?? e.message;
if (e.response.status >= 500) {
Notifications.add("Failed to check name: " + msg, -1);
throw e;
} else {
alert(msg);
}
}
if (response?.status == 200) {
nameGood = true;
const response = await Ape.users.getNameAvailability(name);
if (response.status !== 200) {
return Notifications.add(
"Failed to check name: " + response.message,
-1
);
}
nameGood = true;
}
//create database object for the new user
let response;
// try {
response = await axiosInstance.post("/user/signUp", {
name,
});
const response = Ape.users.create(name);
if (response.status !== 200) {
throw response;
}
// } catch (e) {
// let msg = e?.response?.data?.message ?? e.message;
// Notifications.add("Failed to create account: " + msg, -1);
// return;
// }
if (response.status == 200) {
if (response.status === 200) {
await signedInUser.user.updateProfile({ displayName: name });
await signedInUser.user.sendEmailVerification();
AllTimeStats.clear();
@ -442,20 +428,16 @@ export async function signInWithGoogle() {
PageController.change("account");
if (TestLogic.notSignedInLastResult !== null) {
TestLogic.setNotSignedInUid(signedInUser.user.uid);
axiosInstance
.post("/results/add", {
result: TestLogic.notSignedInLastResult,
})
.then((result) => {
if (result.status === 200) {
const snapshot = DB.getSnapshot();
snapshot.results.push(TestLogic.notSignedInLastResult);
const resultsSaveResponse = await Ape.results.save(
TestLogic.notSignedInLastResult
);
DB.setSnapshot(snapshot);
}
});
// PageController.change("account");
if (resultsSaveResponse.status === 200) {
const snapshot = DB.getSnapshot();
snapshot.results.push(TestLogic.notSignedInLastResult);
DB.setSnapshot(snapshot);
}
}
}
} else {
@ -469,7 +451,7 @@ export async function signInWithGoogle() {
$(".pageLogin .button").removeClass("disabled");
if (signedInUser?.user) {
signedInUser.user.delete();
await axiosInstance.delete("/user");
await Ape.users.delete();
}
return;
}
@ -608,32 +590,24 @@ async function signUp() {
let password = $(".pageLogin .register input")[3].value;
let passwordVerify = $(".pageLogin .register input")[4].value;
if (email != emailVerify) {
if (email !== emailVerify) {
Notifications.add("Emails do not match", 0, 3);
$(".pageLogin .preloader").addClass("hidden");
$(".pageLogin .button").removeClass("disabled");
return;
}
if (password != passwordVerify) {
if (password !== passwordVerify) {
Notifications.add("Passwords do not match", 0, 3);
$(".pageLogin .preloader").addClass("hidden");
$(".pageLogin .button").removeClass("disabled");
return;
}
try {
await axiosInstance.get(`/user/checkName/${nname}`);
} catch (e) {
let txt;
if (e.response) {
txt =
e.response.data.message ||
e.response.status + " " + e.response.statusText;
} else {
txt = e.message;
}
Notifications.add(txt, -1);
const response = await Ape.users.getNameAvailability(nname);
if (response.status !== 200) {
Notifications.add(response.message, -1);
$(".pageLogin .preloader").addClass("hidden");
$(".pageLogin .button").removeClass("disabled");
return;
@ -646,11 +620,16 @@ async function signUp() {
createdAuthUser = await firebase
.auth()
.createUserWithEmailAndPassword(email, password);
await axiosInstance.post("/user/signup", {
name: nname,
const signInResponse = await Ape.users.create(
nname,
email,
uid: createdAuthUser.user.uid,
});
createdAuthUser.user.id
);
if (signInResponse.status !== 200) {
throw signInResponse;
}
await createdAuthUser.user.updateProfile({ displayName: nname });
await createdAuthUser.user.sendEmailVerification();
AllTimeStats.clear();
@ -661,26 +640,21 @@ async function signUp() {
await loadUser(createdAuthUser.user);
if (TestLogic.notSignedInLastResult !== null) {
TestLogic.setNotSignedInUid(createdAuthUser.user.uid);
axiosInstance
.post("/results/add", {
result: TestLogic.notSignedInLastResult,
})
.then((result) => {
if (result.status === 200) {
const snapshot = DB.getSnapshot();
snapshot.results.push(TestLogic.notSignedInLastResult);
const response = await Ape.results.save(TestLogic.notSignedInLastResult);
DB.setSnapshot(snapshot);
}
});
if (response.status === 200) {
const snapshot = DB.getSnapshot();
snapshot.results.push(TestLogic.notSignedInLastResult);
DB.setSnapshot(snapshot);
}
PageController.change("account");
}
} catch (e) {
//make sure to do clean up here
if (createdAuthUser) {
await createdAuthUser.user.delete();
axiosInstance.delete("/user");
await Ape.users.delete();
}
let txt;
if (e.response) {

View file

@ -1,9 +1,8 @@
import Ape from "../ape";
import * as Notifications from "../elements/notifications";
import * as Settings from "../pages/settings";
import * as DB from "../db";
import axiosInstance from "../axios-instance";
import * as Loader from "../elements/loader";
import { AxiosError } from "axios";
type Data = {
accessToken: string;
@ -22,26 +21,20 @@ export async function verify(uid: string): Promise<void> {
Notifications.add("Linking Discord account", 0, 3);
Loader.show();
data.uid = uid;
let response;
try {
response = await axiosInstance.post("/user/discord/link", { data: data });
} catch (error) {
Loader.hide();
const e = error as AxiosError;
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to link Discord: " + msg, -1);
return;
}
const response = await Ape.users.linkDiscord(data);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
Notifications.add("Accounts linked", 1);
const snapshot = DB.getSnapshot();
snapshot.discordId = response.data.did;
DB.setSnapshot(snapshot);
Settings.updateDiscordSection();
return Notifications.add("Failed to link Discord: " + response.message, -1);
}
Notifications.add("Accounts linked", 1);
const snapshot = DB.getSnapshot();
snapshot.discordId = response.data.did;
DB.setSnapshot(snapshot);
Settings.updateDiscordSection();
}

View file

@ -1,19 +1,10 @@
import Ape from "./ape";
import * as AccountButton from "./elements/account-button";
import * as Notifications from "./elements/notifications";
import axiosInstance from "./axios-instance";
import * as LoadingPage from "./pages/loading";
let dbSnapshot: MonkeyTypes.Snapshot;
export function updateName(uid: string, name: string): void {
//TODO update
axiosInstance.patch("/user/name", {
name,
});
name = uid; // this is just so that typescript is happy; Remove once this function is updated.
}
export function getSnapshot(): MonkeyTypes.Snapshot {
return dbSnapshot;
}
@ -66,16 +57,14 @@ export async function initSnapshot(): Promise<
// LoadingPage.updateBar(16);
// }
// LoadingPage.updateText("Downloading user...");
const promises = await Promise.all([
axiosInstance.get("/user"),
axiosInstance.get("/config"),
axiosInstance.get("/user/tags"),
axiosInstance.get("/presets"),
]);
const userData = promises[0].data;
const configData = promises[1].data;
const tagsData = promises[2].data;
const presetsData = promises[3].data;
const [userData, configData, tagsData, presetsData] = (
await Promise.all([
Ape.users.getData(),
Ape.configs.get(),
Ape.users.getTags(),
Ape.presets.get(),
])
).map((response: Ape.Response) => response.data);
snap.name = userData.name;
snap.personalBests = userData.personalBests;
@ -155,70 +144,32 @@ export async function getUserResults(): Promise<boolean> {
if (dbSnapshot.results !== undefined) {
return true;
} else {
try {
LoadingPage.updateText("Downloading results...");
LoadingPage.updateBar(90);
const resultsData = await axiosInstance.get("/results");
LoadingPage.updateText("Downloading results...");
LoadingPage.updateBar(90);
let results = resultsData.data as MonkeyTypes.Result<MonkeyTypes.Mode>[];
const response = await Ape.results.get();
results.forEach((result) => {
if (result.bailedOut === undefined) result.bailedOut = false;
if (result.blindMode === undefined) result.blindMode = false;
if (result.lazyMode === undefined) result.lazyMode = false;
if (result.difficulty === undefined) result.difficulty = "normal";
if (result.funbox === undefined) result.funbox = "none";
if (result.language === undefined || result.language === null)
result.language = "english";
if (result.numbers === undefined) result.numbers = false;
if (result.punctuation === undefined) result.punctuation = false;
});
results = results.sort((a, b) => b.timestamp - a.timestamp);
dbSnapshot.results = results;
return true;
} catch (e: any) {
Notifications.add("Error getting results: " + e.message, -1);
if (response.status !== 200) {
Notifications.add("Error getting results: " + response.message, -1);
return false;
}
}
/*
try {
return await db
.collection(`users/${user.uid}/results/`)
.orderBy("timestamp", "desc")
.limit(1000)
.get()
.then(async (data) => {
dbSnapshot.results = [];
data.docs.forEach((doc) => {
let result = doc.data();
result.id = doc.id;
//this should be done server-side
if (result.bailedOut === undefined) result.bailedOut = false;
if (result.blindMode === undefined) result.blindMode = false;
if (result.difficulty === undefined) result.difficulty = "normal";
if (result.funbox === undefined) result.funbox = "none";
if (result.language === undefined) result.language = "english";
if (result.numbers === undefined) result.numbers = false;
if (result.punctuation === undefined) result.punctuation = false;
dbSnapshot.results.push(result);
});
await TodayTracker.addAllFromToday();
return true;
})
.catch((e) => {
throw e;
});
} catch (e) {
console.error(e);
return false;
}
const results = response.data as MonkeyTypes.Result<MonkeyTypes.Mode>[];
results.forEach((result) => {
if (result.bailedOut === undefined) result.bailedOut = false;
if (result.blindMode === undefined) result.blindMode = false;
if (result.lazyMode === undefined) result.lazyMode = false;
if (result.difficulty === undefined) result.difficulty = "normal";
if (result.funbox === undefined) result.funbox = "none";
if (result.language === undefined || result.language === null)
result.language = "english";
if (result.numbers === undefined) result.numbers = false;
if (result.punctuation === undefined) result.punctuation = false;
});
dbSnapshot.results = results.sort((a, b) => b.timestamp - a.timestamp);
return true;
}
*/
}
export async function getUserHighestWpm<M extends MonkeyTypes.Mode>(
mode: M,
mode2: MonkeyTypes.Mode2<M>,
@ -622,13 +573,13 @@ export async function saveLocalTagPB<M extends MonkeyTypes.Mode>(
return;
}
export function updateLbMemory<M extends MonkeyTypes.Mode>(
export async function updateLbMemory<M extends MonkeyTypes.Mode>(
mode: M,
mode2: MonkeyTypes.Mode2<M>,
language: string,
rank: number,
api = false
): void {
): Promise<void> {
//could dbSnapshot just be used here instead of getSnapshot()
if (mode === "time") {
@ -648,12 +599,7 @@ export function updateLbMemory<M extends MonkeyTypes.Mode>(
const current = snapshot.lbMemory[timeMode][timeMode2][language];
snapshot.lbMemory[timeMode][timeMode2][language] = rank;
if (api && current != rank) {
axiosInstance.patch("/user/leaderboardMemory", {
mode,
mode2,
language,
rank,
});
await Ape.users.updateLeaderboardMemory(mode, mode2, language, rank);
}
setSnapshot(snapshot);
}
@ -662,17 +608,12 @@ export function updateLbMemory<M extends MonkeyTypes.Mode>(
export async function saveConfig(config: MonkeyTypes.Config): Promise<void> {
if (firebase.auth().currentUser !== null) {
AccountButton.loading(true);
try {
await axiosInstance.post("/config/save", { config });
} catch (e: any) {
AccountButton.loading(false);
let msg = e?.response?.data?.message ?? e.message;
if (e.response.status === 429) {
msg = "Too many requests. Please try again later.";
}
Notifications.add("Failed to save config: " + msg, -1);
return;
const response = await Ape.configs.save(config);
if (response.status !== 200) {
Notifications.add("Failed to save config: " + response.message, -1);
}
AccountButton.loading(false);
}
}

View file

@ -1,9 +1,8 @@
import * as Loader from "./loader";
import * as Notifications from "./notifications";
import Ape from "../ape";
import * as DB from "../db";
import axiosInstance from "../axios-instance";
import * as Misc from "../misc";
import Config from "../config";
import * as Misc from "../misc";
import * as Notifications from "./notifications";
const currentLeaderboard = "time_15";
@ -152,33 +151,43 @@ function checkLbMemory(lb: LbKey): void {
side = "right";
}
const memory = DB.getSnapshot()?.lbMemory?.time?.[lb]?.["english"];
const memory = DB.getSnapshot()?.lbMemory?.time?.[lb]?.["english"] ?? 0;
if (memory && currentRank[lb]) {
if (currentRank[lb]) {
const difference = memory - currentRank[lb].rank;
if (difference > 0) {
DB.updateLbMemory("time", lb, "english", currentRank[lb].rank, true);
$(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append(
` (<i class="fas fa-fw fa-angle-up"></i>${Math.abs(
difference
)} since you last checked)`
);
if (memory !== 0) {
$(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append(
` (<i class="fas fa-fw fa-angle-up"></i>${Math.abs(
difference
)} since you last checked)`
);
}
} else if (difference < 0) {
DB.updateLbMemory("time", lb, "english", currentRank[lb].rank, true);
$(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append(
` (<i class="fas fa-fw fa-angle-down"></i>${Math.abs(
difference
)} since you last checked)`
);
if (memory !== 0) {
$(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append(
` (<i class="fas fa-fw fa-angle-down"></i>${Math.abs(
difference
)} since you last checked)`
);
}
} else {
$(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append(
` ( = since you last checked)`
);
if (memory !== 0) {
$(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append(
` ( = since you last checked)`
);
}
}
}
}
function fillTable(lb: LbKey, prepend?: number): void {
if (!currentData[lb]) {
return;
}
let side;
if (lb === 15) {
side = "left";
@ -256,84 +265,58 @@ export function hide(): void {
);
}
function update(): void {
async function update(): Promise<void> {
$("#leaderboardsWrapper .buttons .button").removeClass("active");
$(
`#leaderboardsWrapper .buttons .button[board=${currentLeaderboard}]`
).addClass("active");
// Loader.show();
showLoader(15);
showLoader(60);
const requestsToAwait = [
axiosInstance.get(`/leaderboard`, {
params: {
language: "english",
mode: "time",
mode2: "15",
skip: 0,
},
}),
axiosInstance.get(`/leaderboard`, {
params: {
language: "english",
mode: "time",
mode2: "60",
skip: 0,
},
}),
const leaderboardRequests = [
Ape.leaderboards.get("english", "time", "15", 0),
Ape.leaderboards.get("english", "time", "60", 0),
];
if (firebase.auth().currentUser) {
requestsToAwait.push(
axiosInstance.get(`/leaderboard/rank`, {
params: {
language: "english",
mode: "time",
mode2: "15",
},
})
);
requestsToAwait.push(
axiosInstance.get(`/leaderboard/rank`, {
params: {
language: "english",
mode: "time",
mode2: "60",
},
})
leaderboardRequests.push(
Ape.leaderboards.getRank("english", "time", "15"),
Ape.leaderboards.getRank("english", "time", "60")
);
}
Promise.all(requestsToAwait)
.then((lbdata) => {
// Loader.hide();
hideLoader(15);
hideLoader(60);
currentData[15] = lbdata[0]?.data;
currentData[60] = lbdata[1]?.data;
currentRank[15] = lbdata[2]?.data;
currentRank[60] = lbdata[3]?.data;
const responses = await Promise.all(leaderboardRequests);
clearTable(15);
clearTable(60);
updateFooter(15);
updateFooter(60);
checkLbMemory(15);
checkLbMemory(60);
fillTable(15);
fillTable(60);
$("#leaderboardsWrapper .leftTableWrapper").removeClass("invisible");
$("#leaderboardsWrapper .rightTableWrapper").removeClass("invisible");
})
.catch((e) => {
console.log(e);
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to load leaderboards: " + msg, -1);
return;
});
const failedResponse = responses.find((response) => response.status !== 200);
if (failedResponse) {
return Notifications.add(
"Failed to load leaderboards: " + failedResponse.message,
-1
);
}
const [lb15Data, lb60Data, lb15Rank, lb60Rank] = responses.map(
(response) => response.data
);
currentData[15] = lb15Data;
currentData[60] = lb60Data;
currentRank[15] = lb15Rank;
currentRank[60] = lb60Rank;
const leaderboardKeys: LbKey[] = [15, 60];
leaderboardKeys.forEach((leaderboardTime: LbKey) => {
hideLoader(leaderboardTime);
clearTable(leaderboardTime);
updateFooter(leaderboardTime);
checkLbMemory(leaderboardTime);
fillTable(leaderboardTime);
});
$("#leaderboardsWrapper .leftTableWrapper").removeClass("invisible");
$("#leaderboardsWrapper .rightTableWrapper").removeClass("invisible");
}
async function requestMore(lb: LbKey, prepend = false): Promise<void> {
@ -350,17 +333,17 @@ async function requestMore(lb: LbKey, prepend = false): Promise<void> {
limitVal = Math.abs(skipVal) - 1;
skipVal = 0;
}
const response = await axiosInstance.get(`/leaderboard`, {
params: {
language: "english",
mode: "time",
mode2: lb,
skip: skipVal,
limit: limitVal,
},
});
const response = await Ape.leaderboards.get(
"english",
"time",
lb,
skipVal,
limitVal
);
const data: MonkeyTypes.LeaderboardEntry[] = response.data;
if (data.length === 0) {
if (response.status !== 200 || data.length === 0) {
hideLoader(lb);
return;
}
@ -379,18 +362,13 @@ async function requestMore(lb: LbKey, prepend = false): Promise<void> {
async function requestNew(lb: LbKey, skip: number): Promise<void> {
showLoader(lb);
const response = await axiosInstance.get(`/leaderboard`, {
params: {
language: "english",
mode: "time",
mode2: lb,
skip: skip,
},
});
const response = await Ape.leaderboards.get("english", "time", lb, skip);
const data: MonkeyTypes.LeaderboardEntry[] = response.data;
clearTable(lb);
currentData[lb] = [];
if (data.length === 0) {
if (response.status !== 200 || data.length === 0) {
hideLoader(lb);
return;
}

View file

@ -58,7 +58,7 @@ class Notification {
cls = "bad";
icon = `<i class="fas fa-fw fa-times"></i>`;
title = "Error";
console.trace(this.message);
console.error(this.message);
}
if (this.customTitle != undefined) {

View file

@ -1,4 +1,4 @@
import axiosInstance from "../axios-instance";
import Ape from "../ape";
import * as Notifications from "./notifications";
function clearMemory(): void {
@ -16,8 +16,8 @@ function setMemory(id: string): void {
}
async function getLatest(): Promise<MonkeyTypes.PSA[]> {
const psa = await axiosInstance.get("/psa");
return psa.data;
const response = await Ape.psas.get();
return response.data as MonkeyTypes.PSA[];
}
export async function show(): Promise<void> {

View file

@ -1,11 +1,9 @@
import Ape from "../ape";
import * as DB from "../db";
import * as Config from "../config";
import * as Loader from "../elements/loader";
import axiosInstance from "../axios-instance";
import * as Settings from "../pages/settings";
import * as Notifications from "../elements/notifications";
import { AxiosError } from "axios";
export function show(action: string, id?: string, name?: string): void {
if (action === "add") {
@ -67,115 +65,82 @@ function hide(): void {
async function apply(): Promise<void> {
const action = $("#presetWrapper #presetEdit").attr("action");
const inputVal = $("#presetWrapper #presetEdit input").val() as string;
const presetid = $("#presetWrapper #presetEdit").attr("presetid");
const presetName = $("#presetWrapper #presetEdit input").val() as string;
const presetId = $("#presetWrapper #presetEdit").attr("presetId") as string;
const updateConfig = $("#presetWrapper #presetEdit label input").prop(
"checked"
);
const updateConfig: boolean = $(
"#presetWrapper #presetEdit label input"
).prop("checked");
let configChanges: MonkeyTypes.ConfigChanges = {};
// TODO fix this sometime
let configChanges: MonkeyTypes.PresetConfig =
null as unknown as MonkeyTypes.PresetConfig;
if ((updateConfig && action === "edit") || action === "add") {
configChanges = Config.getConfigChanges();
const activeTagIds: string[] = [];
DB.getSnapshot().tags?.forEach((tag) => {
if (tag.active) {
activeTagIds.push(tag._id);
}
});
configChanges.tags = activeTagIds as string[];
const tags = DB.getSnapshot().tags || [];
const activeTagIds: string[] = tags.filter((tag: MonkeyTypes.Tag) => tag.active)
.map((tag: MonkeyTypes.Tag) => tag._id);
configChanges.tags = activeTagIds;
}
const snapshotPresets = DB.getSnapshot().presets || [];
hide();
Loader.show();
if (action === "add") {
Loader.show();
let response;
try {
response = await axiosInstance.post("/presets/add", {
name: inputVal,
config: configChanges,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to add preset: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.presets.add(presetName, configChanges);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to add preset: " + response.message, -1);
} else {
Notifications.add("Preset added", 1, 2);
DB.getSnapshot().presets?.push({
name: inputVal,
snapshotPresets.push({
name: presetName,
config: configChanges,
_id: response.data.insertedId,
});
Settings.update();
}
} else if (action === "edit") {
Loader.show();
let response;
try {
response = await axiosInstance.post("/presets/edit", {
name: inputVal,
_id: presetid,
config: updateConfig === true ? configChanges : null,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to edit preset: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.presets.edit(
presetId,
presetName,
configChanges
);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to edit preset: " + response.message, -1);
} else {
Notifications.add("Preset updated", 1);
const preset = DB.getSnapshot().presets?.filter(
(preset: MonkeyTypes.Preset) => preset._id == presetid
const preset: MonkeyTypes.Preset = snapshotPresets.filter(
(preset: MonkeyTypes.Preset) => preset._id === presetId
)[0];
if (preset !== undefined) {
preset.name = inputVal;
if (updateConfig === true) preset.config = configChanges;
Settings.update();
preset.name = presetName;
if (updateConfig) {
preset.config = configChanges;
}
}
} else if (action === "remove") {
Loader.show();
let response;
try {
response = await axiosInstance.post("/presets/remove", {
_id: presetid,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to remove preset: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.presets.delete(presetId);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to remove preset: " + response.message, -1);
} else {
Notifications.add("Preset removed", 1);
DB.getSnapshot().presets?.forEach(
snapshotPresets.forEach(
(preset: MonkeyTypes.Preset, index: number) => {
if (preset._id === presetid) {
DB.getSnapshot().presets?.splice(index, 1);
if (preset._id === presetId) {
snapshotPresets.splice(index, 1);
}
}
);
Settings.update();
}
}
Settings.update();
Loader.hide();
}
$("#presetWrapper").click((e) => {
@ -189,7 +154,7 @@ $("#presetWrapper #presetEdit .button").click(() => {
});
$("#presetWrapper #presetEdit input").keypress((e) => {
if (e.keyCode == 13) {
if (e.keyCode === 13) {
apply();
}
});

View file

@ -1,13 +1,11 @@
import Ape from "../ape";
import * as ResultFilters from "../account/result-filters";
import * as DB from "../db";
import * as Notifications from "../elements/notifications";
import * as Loader from "../elements/loader";
import * as Settings from "../pages/settings";
import axiosInstance from "../axios-instance";
import * as ResultTagsPopup from "./result-tags-popup";
import { AxiosError } from "axios";
export function show(action: string, id?: string, name?: string): void {
if (action === "add") {
$("#tagsWrapper #tagsEdit").attr("action", "add");
@ -67,28 +65,18 @@ function hide(): void {
}
async function apply(): Promise<void> {
// console.log(DB.getSnapshot());
const action = $("#tagsWrapper #tagsEdit").attr("action");
const inputVal = $("#tagsWrapper #tagsEdit input").val() as string;
const tagid = $("#tagsWrapper #tagsEdit").attr("tagid");
const tagName = $("#tagsWrapper #tagsEdit input").val() as string;
const tagId = $("#tagsWrapper #tagsEdit").attr("tagid") as string;
hide();
Loader.show();
if (action === "add") {
Loader.show();
let response;
try {
response = await axiosInstance.post("/user/tags", {
tagName: inputVal,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to add tag: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.users.createTag(tagName);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to add tag: " + response.message, -1);
} else {
Notifications.add("Tag added", 1);
DB.getSnapshot().tags?.push({
@ -100,28 +88,15 @@ async function apply(): Promise<void> {
ResultFilters.updateTags();
}
} else if (action === "edit") {
Loader.show();
let response;
try {
response = await axiosInstance.patch("/user/tags", {
tagId: tagid,
newName: inputVal,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to edit tag: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.users.editTag(tagId, tagName);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to edit tag: " + response.message, -1);
} else {
Notifications.add("Tag updated", 1);
DB.getSnapshot().tags?.forEach((tag) => {
if (tag._id === tagid) {
tag.name = inputVal;
if (tag._id === tagId) {
tag.name = tagName;
}
});
ResultTagsPopup.updateButtons();
@ -129,24 +104,14 @@ async function apply(): Promise<void> {
ResultFilters.updateTags();
}
} else if (action === "remove") {
Loader.show();
let response;
try {
response = await axiosInstance.delete(`/user/tags/${tagid}`);
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to remove tag: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.users.deleteTag(tagId);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to remove tag: " + response.message, -1);
} else {
Notifications.add("Tag removed", 1);
DB.getSnapshot().tags?.forEach((tag, index: number) => {
if (tag._id === tagid) {
if (tag._id === tagId) {
DB.getSnapshot().tags?.splice(index, 1);
}
});
@ -155,24 +120,14 @@ async function apply(): Promise<void> {
ResultFilters.updateTags();
}
} else if (action === "clearPb") {
Loader.show();
let response;
try {
response = await axiosInstance.delete(`/user/tags/${tagid}/personalBest`);
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to clear tag pb: " + msg, -1);
return;
}
Loader.hide();
const response = await Ape.users.deleteTagPersonalBest(tagId);
if (response.status !== 200) {
Notifications.add(response.data.message);
Notifications.add("Failed to clear tag pb: " + response.message, -1);
} else {
Notifications.add("Tag PB cleared", 1);
DB.getSnapshot().tags?.forEach((tag) => {
if (tag._id === tagid) {
if (tag._id === tagId) {
tag.personalBests = {
time: {},
words: {},
@ -187,6 +142,7 @@ async function apply(): Promise<void> {
ResultFilters.updateTags();
}
}
Loader.hide();
}
$("#tagsWrapper").click((e) => {

View file

@ -1,7 +1,6 @@
import Ape from "../ape";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import axiosInstance from "../axios-instance";
import { AxiosError } from "axios";
type Quote = {
_id: string;
@ -59,23 +58,18 @@ function updateQuoteLength(index: number): void {
async function getQuotes(): Promise<void> {
Loader.show();
let response;
try {
response = await axiosInstance.get("/quotes");
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to get new quotes: " + msg, -1);
return;
}
const response = await Ape.quotes.get();
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
quotes = response.data;
updateList();
return Notifications.add(
"Failed to get new quotes: " + response.message,
-1
);
}
quotes = response.data;
updateList();
}
export async function show(noAnim = false): Promise<void> {
@ -139,110 +133,86 @@ $(document).on("click", "#quoteApprovePopup .quote .undo", async (e) => {
$(document).on("click", "#quoteApprovePopup .quote .approve", async (e) => {
if (!confirm("Are you sure?")) return;
const index = parseInt($(e.target).closest(".quote").attr("id") as string);
const dbid = $(e.target).closest(".quote").attr("dbid");
const dbid = $(e.target).closest(".quote").attr("dbid") as string;
const target = e.target;
$(target).closest(".quote").find(".icon-button").addClass("disabled");
$(target).closest(".quote").find("textarea, input").prop("disabled", true);
Loader.show();
let response;
try {
response = await axiosInstance.post("/quotes/approve", {
quoteId: dbid,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to approve quote: " + msg, -1);
resetButtons(target);
$(target).closest(".quote").find("textarea, input").prop("disabled", false);
return;
}
const response = await Ape.quotes.approveSubmission(dbid);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
resetButtons(target);
$(target).closest(".quote").find("textarea, input").prop("disabled", false);
} else {
Notifications.add("Quote approved. " + response.data.message ?? "", 1);
quotes.splice(index, 1);
updateList();
return Notifications.add(
"Failed to approve quote: " + response.message,
-1
);
}
Notifications.add("Quote approved. " + response.message ?? "", 1);
quotes.splice(index, 1);
updateList();
});
$(document).on("click", "#quoteApprovePopup .quote .refuse", async (e) => {
if (!confirm("Are you sure?")) return;
const index = parseInt($(e.target).closest(".quote").attr("id") as string);
const dbid = $(e.target).closest(".quote").attr("dbid");
const dbid = $(e.target).closest(".quote").attr("dbid") as string;
const target = e.target;
$(target).closest(".quote").find(".icon-button").addClass("disabled");
$(target).closest(".quote").find("textarea, input").prop("disabled", true);
Loader.show();
let response;
try {
response = await axiosInstance.post("/quotes/reject", {
quoteId: dbid,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to refuse quote: " + msg, -1);
resetButtons(target);
$(target).closest(".quote").find("textarea, input").prop("disabled", false);
return;
}
const response = await Ape.quotes.rejectSubmission(dbid);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
resetButtons(target);
$(target).closest(".quote").find("textarea, input").prop("disabled", false);
} else {
Notifications.add("Quote refused.", 1);
quotes.splice(index, 1);
updateList();
return Notifications.add("Failed to refuse quote: " + response.message, -1);
}
Notifications.add("Quote refused.", 1);
quotes.splice(index, 1);
updateList();
});
$(document).on("click", "#quoteApprovePopup .quote .edit", async (e) => {
if (!confirm("Are you sure?")) return;
const index = parseInt($(e.target).closest(".quote").attr("id") as string);
const dbid = $(e.target).closest(".quote").attr("dbid");
const editText = $(`#quoteApprovePopup .quote[id=${index}] .text`).val();
const editSource = $(`#quoteApprovePopup .quote[id=${index}] .source`).val();
const dbid = $(e.target).closest(".quote").attr("dbid") as string;
const editText = $(
`#quoteApprovePopup .quote[id=${index}] .text`
).val() as string;
const editSource = $(
`#quoteApprovePopup .quote[id=${index}] .source`
).val() as string;
const target = e.target;
$(target).closest(".quote").find(".icon-button").addClass("disabled");
$(target).closest(".quote").find("textarea, input").prop("disabled", true);
Loader.show();
let response;
try {
response = await axiosInstance.post("/quotes/approve", {
quoteId: dbid,
editText,
editSource,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to approve quote: " + msg, -1);
resetButtons(target);
$(target).closest(".quote").find("textarea, input").prop("disabled", false);
return;
}
const response = await Ape.quotes.approveSubmission(
dbid,
editText,
editSource
);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
resetButtons(target);
$(target).closest(".quote").find("textarea, input").prop("disabled", false);
} else {
Notifications.add(
"Quote edited and approved. " + response.data.message ?? "",
1
return Notifications.add(
"Failed to approve quote: " + response.message,
-1
);
quotes.splice(index, 1);
updateList();
}
Notifications.add("Quote edited and approved. " + response.message ?? "", 1);
quotes.splice(index, 1);
updateList();
});
$(document).on("input", "#quoteApprovePopup .quote .text", async (e) => {

View file

@ -1,18 +1,17 @@
import Ape from "../ape";
import * as DB from "../db";
import * as TestWords from "../test/test-words";
import * as Loader from "../elements/loader";
import axiosInstance from "../axios-instance";
import * as Notifications from "../elements/notifications";
import { AxiosError } from "axios";
let rating = 0;
type QuoteStats = {
average: number;
ratings: number;
totalRating: number;
quoteId: number;
language: string;
average?: number;
ratings?: number;
totalRating?: number;
quoteId?: number;
language?: string;
};
let quoteStats: QuoteStats | null | Record<string, never> = null;
@ -27,39 +26,40 @@ function reset(): void {
$("#quoteRatePopup .ratingAverage .val").text("-");
}
function getRatingAverage(quoteStats: QuoteStats): number {
if (!quoteStats.totalRating || !quoteStats.ratings) {
return 0;
}
return Math.round((quoteStats.totalRating / quoteStats.ratings) * 10) / 10;
}
export async function getQuoteStats(
quote?: MonkeyTypes.Quote
): Promise<QuoteStats | undefined> {
if (quote) currentQuote = quote;
let response;
try {
response = await axiosInstance.get("/quotes/rating", {
params: { quoteId: currentQuote?.id, language: currentQuote?.language },
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to get quote ratings: " + msg, -1);
if (!quote) {
return;
}
currentQuote = quote;
const response = await Ape.quotes.getRating(currentQuote);
Loader.hide();
if (response.status !== 200 && response.status !== 204) {
Notifications.add(response.data.message);
} else {
if (response.status === 204) {
quoteStats = {};
} else {
quoteStats = response.data;
if (quoteStats && !quoteStats.average) {
quoteStats.average =
Math.round((quoteStats.totalRating / quoteStats.ratings) * 10) / 10;
}
}
return quoteStats as QuoteStats;
if (response.status !== 200) {
Notifications.add("Failed to get quote ratings: " + response.message, -1);
return;
}
return;
if (!response.data) {
return {} as QuoteStats;
}
quoteStats = response.data as QuoteStats;
if (quoteStats && !quoteStats.average) {
quoteStats.average = getRatingAverage(quoteStats);
}
return quoteStats;
}
function refreshStars(force?: number): void {
@ -107,11 +107,8 @@ export function show(quote: MonkeyTypes.Quote, shouldReset = true): void {
rating = 0;
const snapshot = DB.getSnapshot();
if (snapshot.quoteRatings === undefined) return;
const alreadyRated =
snapshot.quoteRatings[currentQuote.language][currentQuote.id];
snapshot?.quoteRatings?.[currentQuote.language]?.[currentQuote.id];
if (alreadyRated) {
rating = alreadyRated;
}
@ -148,73 +145,69 @@ export function clearQuoteStats(): void {
}
async function submit(): Promise<void> {
if (rating == 0) {
Notifications.add("Please select a rating");
if (rating === 0) {
return Notifications.add("Please select a rating");
}
if (!currentQuote) {
return;
}
if (!currentQuote) return;
hide();
let response;
try {
response = await axiosInstance.post("/quotes/rating", {
quoteId: currentQuote?.id,
rating: rating,
language: currentQuote?.language,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to submit quote rating: " + msg, -1);
const response = await Ape.quotes.addRating(currentQuote, rating);
Loader.hide();
if (response.status !== 200) {
return Notifications.add(
"Failed to submit quote rating: " + response.message,
-1
);
}
const quoteRatings = DB.getSnapshot().quoteRatings;
if (quoteRatings === undefined) {
return;
}
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
if (quoteRatings?.[currentQuote.language]?.[currentQuote.id]) {
const oldRating = quoteRatings[currentQuote.language][currentQuote.id];
const diff = rating - oldRating;
quoteRatings[currentQuote.language][currentQuote.id] = rating;
quoteStats = {
ratings: quoteStats?.ratings,
totalRating: isNaN(quoteStats?.totalRating as number)
? 0
: (quoteStats?.totalRating as number) + diff,
quoteId: currentQuote.id,
language: currentQuote.language,
} as QuoteStats;
Notifications.add("Rating updated", 1);
} else {
let quoteRatings = DB.getSnapshot().quoteRatings;
if (quoteRatings === undefined) return;
if (quoteRatings?.[currentQuote.language]?.[currentQuote.id]) {
const oldRating = quoteRatings[currentQuote.language][currentQuote.id];
const diff = rating - oldRating;
quoteRatings[currentQuote.language][currentQuote.id] = rating;
if (quoteRatings[currentQuote.language] === undefined) {
quoteRatings[currentQuote.language] = {};
}
quoteRatings[currentQuote.language][currentQuote.id] = rating;
if (quoteStats?.ratings && quoteStats.totalRating) {
quoteStats.ratings++;
quoteStats.totalRating += rating;
} else {
quoteStats = {
ratings: quoteStats?.ratings,
totalRating: isNaN(quoteStats?.totalRating as number)
? 0
: (quoteStats?.totalRating as number) + diff,
ratings: 1,
totalRating: rating,
quoteId: currentQuote.id,
language: currentQuote.language,
} as QuoteStats;
Notifications.add("Rating updated", 1);
} else {
if (quoteRatings === undefined) quoteRatings = {};
if (quoteRatings[currentQuote.language] === undefined)
quoteRatings[currentQuote.language] = {};
quoteRatings[currentQuote.language][currentQuote.id] = rating;
if (quoteStats?.ratings && quoteStats.totalRating) {
quoteStats.ratings++;
quoteStats.totalRating += rating;
} else {
quoteStats = {
ratings: 1,
totalRating: rating,
quoteId: currentQuote.id,
language: currentQuote.language,
} as QuoteStats;
}
Notifications.add("Rating submitted", 1);
}
quoteStats.average =
Math.round((quoteStats.totalRating / quoteStats.ratings) * 10) / 10;
$(".pageTest #result #rateQuoteButton .rating").text(
quoteStats.average?.toFixed(1)
);
$(".pageTest #result #rateQuoteButton .icon").removeClass("far");
$(".pageTest #result #rateQuoteButton .icon").addClass("fas");
Notifications.add("Rating submitted", 1);
}
quoteStats.average = getRatingAverage(quoteStats);
$(".pageTest #result #rateQuoteButton .rating").text(
quoteStats.average?.toFixed(1)
);
$(".pageTest #result #rateQuoteButton .icon").removeClass("far");
$(".pageTest #result #rateQuoteButton .icon").addClass("fas");
}
$("#quoteRatePopupWrapper").click((e) => {

View file

@ -1,12 +1,10 @@
import Ape from "../ape";
import Config from "../config";
import * as TestWords from "../test/test-words";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import axiosInstance from "../axios-instance";
import * as Misc from "../misc";
import { AxiosError } from "axios";
const CAPTCHA_ID = 1;
type State = {
@ -87,51 +85,46 @@ export async function hide(): Promise<void> {
async function submitReport(): Promise<void> {
const captchaResponse = grecaptcha.getResponse(CAPTCHA_ID);
if (!captchaResponse) {
Notifications.add("Please complete the captcha.");
return;
return Notifications.add("Please complete the captcha.");
}
const requestBody = {
quoteId: state.quoteToReport?.id.toString(),
quoteLanguage: Config.language,
reason: $("#quoteReportPopup .reason").val(),
comment: $("#quoteReportPopup .comment").val() as string,
captcha: captchaResponse,
};
const quoteId = state.quoteToReport?.id.toString();
const quoteLanguage = Config.language;
const reason = $("#quoteReportPopup .reason").val() as string;
const comment = $("#quoteReportPopup .comment").val() as string;
const captcha = captchaResponse as string;
if (!requestBody.reason) {
Notifications.add("Please select a valid report reason.");
return;
if (!quoteId) {
return Notifications.add("Please select a quote.");
}
const characterDifference = requestBody.comment.length - 250;
if (!reason) {
return Notifications.add("Please select a valid report reason.");
}
const characterDifference = comment.length - 250;
if (characterDifference > 0) {
Notifications.add(
return Notifications.add(
`Report comment is ${characterDifference} character(s) too long.`
);
return;
}
Loader.show();
let response;
try {
response = await axiosInstance.post("/quotes/report", requestBody);
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to report quote: " + msg, -1);
return;
}
const response = await Ape.quotes.report(
quoteId,
quoteLanguage,
reason,
comment,
captcha
);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
Notifications.add("Report submitted. Thank you!", 1);
hide();
return Notifications.add("Failed to report quote: " + response.message, -1);
}
Notifications.add("Report submitted. Thank you!", 1);
hide();
}
$("#quoteReportPopupWrapper").on("mousedown", (e) => {

View file

@ -1,9 +1,8 @@
// import Config from "../config";
import Ape from "../ape";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
// import Config from "../config";
// import * as Misc from "../misc";
import axiosInstance from "../axios-instance";
import { AxiosError } from "axios";
// let dropdownReady = false;
// async function initDropdown(): Promise<void> {
@ -26,40 +25,29 @@ import { AxiosError } from "axios";
// }
async function submitQuote(): Promise<void> {
const data = {
text: $("#quoteSubmitPopup #submitQuoteText").val(),
source: $("#quoteSubmitPopup #submitQuoteSource").val(),
language: $("#quoteSubmitPopup #submitQuoteLanguage").val(),
captcha: $("#quoteSubmitPopup #g-recaptcha-response").val(),
};
const text = $("#quoteSubmitPopup #submitQuoteText").val() as string;
const source = $("#quoteSubmitPopup #submitQuoteSource").val() as string;
const language = $("#quoteSubmitPopup #submitQuoteLanguage").val() as string;
const captcha = $("#quoteSubmitPopup #g-recaptcha-response").val() as string;
if (!data.text || !data.source || !data.language) {
Notifications.add("Please fill in all fields", 0);
return;
if (!text || !source || !language) {
return Notifications.add("Please fill in all fields", 0);
}
Loader.show();
let response;
try {
response = await axiosInstance.post("/quotes", data);
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to submit quote: " + msg, -1);
return;
}
const response = await Ape.quotes.submit(text, source, language, captcha);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
Notifications.add("Quote submitted.", 1);
$("#quoteSubmitPopup #submitQuoteText").val("");
$("#quoteSubmitPopup #submitQuoteSource").val("");
$("#quoteSubmitPopup .characterCount").removeClass("red");
$("#quoteSubmitPopup .characterCount").text("-");
grecaptcha.reset();
return Notifications.add("Failed to submit quote: " + response.message, -1);
}
Notifications.add("Quote submitted.", 1);
$("#quoteSubmitPopup #submitQuoteText").val("");
$("#quoteSubmitPopup #submitQuoteSource").val("");
$("#quoteSubmitPopup .characterCount").removeClass("red");
$("#quoteSubmitPopup .characterCount").text("-");
grecaptcha.reset();
}
// @ts-ignore

View file

@ -1,7 +1,7 @@
import Ape from "../ape";
import * as DB from "../db";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import axiosInstance from "../axios-instance";
function show(): void {
if ($("#resultEditTagsPanelWrapper").hasClass("hidden")) {
@ -72,87 +72,77 @@ $("#resultEditTagsPanelWrapper").click((e) => {
}
});
$("#resultEditTagsPanel .confirmButton").click(() => {
const resultid = $("#resultEditTagsPanel").attr("resultid");
$("#resultEditTagsPanel .confirmButton").click(async () => {
const resultId = $("#resultEditTagsPanel").attr("resultid") as string;
// let oldtags = JSON.parse($("#resultEditTagsPanel").attr("tags"));
const newtags: string[] = [];
const newTags: string[] = [];
$.each($("#resultEditTagsPanel .buttons .button"), (_, obj) => {
const tagid = $(obj).attr("tagid") ?? "";
if ($(obj).hasClass("active")) {
newtags.push(tagid);
newTags.push(tagid);
}
});
Loader.show();
hide();
axiosInstance
.post("/results/updateTags", {
tags: newtags,
resultid: resultid,
})
.then((response) => {
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
Notifications.add("Tags updated.", 1, 2);
DB.getSnapshot().results?.forEach(
(result: MonkeyTypes.Result<MonkeyTypes.Mode>) => {
if (result._id === resultid) {
result.tags = newtags;
}
}
);
const response = await Ape.results.updateTags(resultId, newTags);
let tagNames = "";
Loader.hide();
if (response.status !== 200) {
return Notifications.add(
"Failed to update result tags: " + response.message,
-1
);
}
if (newtags.length > 0) {
newtags.forEach((tag) => {
DB.getSnapshot().tags?.forEach((snaptag) => {
if (tag === snaptag._id) {
tagNames += snaptag.name + ", ";
}
});
});
tagNames = tagNames.substring(0, tagNames.length - 2);
}
let restags;
if (newtags === undefined) {
restags = "[]";
} else {
restags = JSON.stringify(newtags);
}
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"tags",
restags
);
if (newtags.length > 0) {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
1
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
tagNames
);
} else {
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).css(
"opacity",
0.25
);
$(`.pageAccount #resultEditTags[resultid='${resultid}']`).attr(
"aria-label",
"no tags"
);
}
Notifications.add("Tags updated.", 1, 2);
DB.getSnapshot().results?.forEach(
(result: MonkeyTypes.Result<MonkeyTypes.Mode>) => {
if (result._id === resultId) {
result.tags = newTags;
}
})
.catch((e) => {
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to update result tags: " + msg, -1);
}
);
let tagNames = "";
if (newTags.length > 0) {
newTags.forEach((tag) => {
DB.getSnapshot().tags?.forEach((snaptag) => {
if (tag === snaptag._id) {
tagNames += snaptag.name + ", ";
}
});
});
tagNames = tagNames.substring(0, tagNames.length - 2);
}
let restags;
if (newTags === undefined) {
restags = "[]";
} else {
restags = JSON.stringify(newTags);
}
$(`.pageAccount #resultEditTags[resultid='${resultId}']`).attr(
"tags",
restags
);
if (newTags.length > 0) {
$(`.pageAccount #resultEditTags[resultid='${resultId}']`).css("opacity", 1);
$(`.pageAccount #resultEditTags[resultid='${resultId}']`).attr(
"aria-label",
tagNames
);
} else {
$(`.pageAccount #resultEditTags[resultid='${resultId}']`).css(
"opacity",
0.25
);
$(`.pageAccount #resultEditTags[resultid='${resultId}']`).attr(
"aria-label",
"no tags"
);
}
});

View file

@ -1,11 +1,10 @@
import Ape from "../ape";
import * as AccountController from "../controllers/account-controller";
import * as DB from "../db";
import * as UpdateConfig from "../config";
import * as Loader from "../elements/loader";
import * as Notifications from "../elements/notifications";
import axiosInstance from "../axios-instance";
import * as Settings from "../pages/settings";
import { AxiosError } from "axios";
type Input = {
placeholder: string;
@ -235,31 +234,22 @@ list["updateEmail"] = new SimplePopup(
);
await user.reauthenticateWithCredential(credential);
}
Loader.show();
let response;
try {
response = await axiosInstance.patch("/user/email", {
uid: user.uid,
previousEmail: user.email,
newEmail: email,
});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to update email: " + msg, -1);
return;
}
const response = await Ape.users.updateEmail(email, user.email);
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
return;
} else {
Notifications.add("Email updated", 1);
setTimeout(() => {
window.location.reload();
}, 1000);
return Notifications.add(
"Failed to update email: " + response.message,
-1
);
}
Notifications.add("Email updated", 1);
setTimeout(() => {
window.location.reload();
}, 1000);
} catch (e) {
// @ts-ignore todo help
if (e.code == "auth/wrong-password") {
@ -312,50 +302,36 @@ list["updateName"] = new SimplePopup(
}
Loader.show();
let response;
try {
response = await axiosInstance.get(`/user/checkName/${newName}`);
} catch (error) {
const e = error as AxiosError;
const checkNameResponse = await Ape.users.getNameAvailability(newName);
if (checkNameResponse.status !== 200) {
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to check name: " + msg, -1);
return;
return Notifications.add(
"Failed to check name: " + checkNameResponse.message,
-1
);
}
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
return;
}
try {
response = await axiosInstance.patch("/user/name", {
name: newName,
});
} catch (error) {
const e = error as AxiosError;
const updateNameResponse = await Ape.users.updateName(newName);
if (updateNameResponse.status !== 200) {
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to update name: " + msg, -1);
return;
}
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
return;
} else {
Notifications.add("Name updated", 1);
DB.getSnapshot().name = newName;
$("#menu .icon-button.account .text").text(newName);
return Notifications.add(
"Failed to update name: " + updateNameResponse.message,
-1
);
}
Notifications.add("Name updated", 1);
DB.getSnapshot().name = newName;
$("#menu .icon-button.account .text").text(newName);
} catch (e) {
Loader.hide();
// @ts-ignore todo remove ignore
if (e.code == "auth/wrong-password") {
if (e.code === "auth/wrong-password") {
Notifications.add("Incorrect password", -1);
} else {
Notifications.add("Something went wrong: " + e, -1);
}
}
Loader.hide();
},
(thisPopup) => {
const user = firebase.auth().currentUser;
@ -505,34 +481,27 @@ list["deleteAccount"] = new SimplePopup(
await user.reauthenticateWithPopup(AccountController.gmailProvider);
}
Loader.show();
Notifications.add("Deleting stats...", 0);
let response;
try {
response = await axiosInstance.delete("/user");
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to delete user stats: " + msg, -1);
return;
}
if (response.status !== 200) {
throw response.data.message;
const usersResponse = await Ape.users.delete();
Loader.hide();
if (usersResponse.status !== 200) {
return Notifications.add(
"Failed to delete user stats: " + usersResponse.message,
-1
);
}
Loader.show();
Notifications.add("Deleting results...", 0);
try {
response = await axiosInstance.post("/results/deleteAll");
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to delete user results: " + msg, -1);
return;
}
if (response.status !== 200) {
throw response.data.message;
const resultsResponse = await Ape.results.deleteAll();
Loader.hide();
if (resultsResponse.status !== 200) {
return Notifications.add(
"Failed to delete user results: " + resultsResponse.message,
-1
);
}
Notifications.add("Deleting login information...", 0);
@ -569,40 +538,37 @@ list["clearTagPb"] = new SimplePopup(
[],
`Are you sure you want to clear this tags PB?`,
"Clear",
(thisPopup) => {
const tagid = thisPopup.parameters[0];
async (thisPopup) => {
const tagId = thisPopup.parameters[0];
Loader.show();
axiosInstance
.delete(`/user/tags/${tagid}/clearPb`)
.then((res) => {
Loader.hide();
if (res.data.resultCode === 1) {
const tag = DB.getSnapshot().tags?.filter((t) => t._id === tagid)[0];
const response = await Ape.users.deleteTagPersonalBest(tagId);
Loader.hide();
if (tag === undefined) return;
tag.personalBests = {
time: {},
words: {},
zen: { zen: [] },
quote: { custom: [] },
custom: { custom: [] },
};
$(
`.pageSettings .section.tags .tagsList .tag[id="${tagid}"] .clearPbButton`
).attr("aria-label", "No PB found");
Notifications.add("Tag PB cleared.", 0);
} else {
Notifications.add("Something went wrong: " + res.data.message, -1);
}
})
.catch((e) => {
Loader.hide();
if (e.code == "auth/wrong-password") {
Notifications.add("Incorrect password", -1);
} else {
Notifications.add("Something went wrong: " + e, -1);
}
});
if (response.status !== 200) {
return Notifications.add(
"Failed to delete tag's PB: " + response.message
);
}
if (response.data.resultCode === 1) {
const tag = DB.getSnapshot().tags?.filter((t) => t._id === tagId)[0];
if (tag === undefined) return;
tag.personalBests = {
time: {},
words: {},
zen: { zen: [] },
quote: { custom: [] },
custom: { custom: [] },
};
$(
`.pageSettings .section.tags .tagsList .tag[id="${tagId}"] .clearPbButton`
).attr("aria-label", "No PB found");
Notifications.add("Tag PB cleared.", 0);
} else {
Notifications.add("Something went wrong: " + response.message, -1);
}
// console.log(`clearing for ${eval("this.parameters[0]")} ${eval("this.parameters[1]")}`);
},
(thisPopup) => {
thisPopup.text = `Are you sure you want to clear PB for tag ${thisPopup.parameters[1]}?`;
@ -651,30 +617,24 @@ list["resetPersonalBests"] = new SimplePopup(
await user.reauthenticateWithPopup(AccountController.gmailProvider);
}
Loader.show();
let response;
try {
response = await axiosInstance.delete("/user/personalBests");
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to reset personal bests: " + msg, -1);
return;
}
const response = await Ape.users.deletePersonalBests();
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
Notifications.add("Personal bests have been reset", 1);
DB.getSnapshot().personalBests = {
time: {},
words: {},
zen: { zen: [] },
quote: { custom: [] },
custom: { custom: [] },
};
return Notifications.add(
"Failed to reset personal bests: " + response.message,
-1
);
}
Notifications.add("Personal bests have been reset", 1);
DB.getSnapshot().personalBests = {
time: {},
words: {},
zen: { zen: [] },
quote: { custom: [] },
custom: { custom: [] },
};
} catch (e) {
Loader.hide();
Notifications.add(e as string, -1);
@ -716,24 +676,19 @@ list["unlinkDiscord"] = new SimplePopup(
"Unlink",
async () => {
Loader.show();
let response;
try {
response = await axiosInstance.post("/user/discord/unlink", {});
} catch (error) {
const e = error as AxiosError;
Loader.hide();
const msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to unlink Discord: " + msg, -1);
return;
}
const response = await Ape.users.unlinkDiscord();
Loader.hide();
if (response.status !== 200) {
Notifications.add(response.data.message);
} else {
Notifications.add("Accounts unlinked", 1);
DB.getSnapshot().discordId = undefined;
Settings.updateDiscordSection();
return Notifications.add(
"Failed to unlink Discord: " + response.message,
-1
);
}
Notifications.add("Accounts unlinked", 1);
DB.getSnapshot().discordId = undefined;
Settings.updateDiscordSection();
},
() => {
//

View file

@ -1,3 +1,4 @@
import Ape from "../ape";
import * as TestUI from "./test-ui";
import * as ManualRestart from "./manual-restart-tracker";
import Config, * as UpdateConfig from "../config";
@ -25,7 +26,6 @@ import * as OutOfFocus from "./out-of-focus";
import * as AccountButton from "../elements/account-button";
import * as DB from "../db";
import * as Replay from "./replay.js";
import axiosInstance from "../axios-instance";
import * as Poetry from "./poetry.js";
import * as Wikipedia from "./wikipedia.js";
import * as TodayTracker from "./today-tracker";
@ -1058,7 +1058,7 @@ let retrySaving = {
canRetry: false,
};
export function retrySavingResult() {
export async function retrySavingResult() {
if (!retrySaving.completedEvent) {
Notifications.add(
"Could not retry saving the result as the result no longer exists.",
@ -1079,66 +1079,57 @@ export function retrySavingResult() {
let { completedEvent } = retrySaving;
axiosInstance
.post("/results/add", {
result: completedEvent,
})
.then((response) => {
AccountButton.loading(false);
Result.hideCrown();
const response = await Ape.results.save(completedEvent);
if (response.status !== 200) {
Notifications.add("Result not saved. " + response.data.message, -1);
} else {
completedEvent._id = response.data.insertedId;
if (response.data.isPb) {
completedEvent.isPb = true;
}
AccountButton.loading(false);
Result.hideCrown();
DB.saveLocalResult(completedEvent);
DB.updateLocalStats({
time:
completedEvent.testDuration +
completedEvent.incompleteTestSeconds -
completedEvent.afkDuration,
started: TestStats.restartCount + 1,
});
if (response.status !== 200) {
retrySaving.canRetry = true;
$("#retrySavingResultButton").removeClass("hidden");
return Notifications.add("Result not saved. " + response.data.message, -1);
}
try {
firebase.analytics().logEvent("testCompleted", completedEvent);
} catch (e) {
console.log("Analytics unavailable");
}
completedEvent._id = response.data.insertedId;
if (response.data.isPb) {
completedEvent.isPb = true;
}
if (response.data.isPb) {
//new pb
Result.showCrown();
Result.updateCrown();
DB.saveLocalPB(
Config.mode,
completedEvent.mode2,
Config.punctuation,
Config.language,
Config.difficulty,
Config.lazyMode,
completedEvent.wpm,
completedEvent.acc,
completedEvent.rawWpm,
completedEvent.consistency
);
}
}
DB.saveLocalResult(completedEvent);
DB.updateLocalStats({
time:
completedEvent.testDuration +
completedEvent.incompleteTestSeconds -
completedEvent.afkDuration,
started: TestStats.restartCount + 1,
});
$("#retrySavingResultButton").addClass("hidden");
Notifications.add("Result saved", 1);
})
.catch((e) => {
AccountButton.loading(false);
let msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to save result: " + msg, -1);
$("#retrySavingResultButton").removeClass("hidden");
retrySaving.canRetry = true;
});
try {
firebase.analytics().logEvent("testCompleted", completedEvent);
} catch (e) {
console.log("Analytics unavailable");
}
if (response.data.isPb) {
//new pb
Result.showCrown();
Result.updateCrown();
DB.saveLocalPB(
Config.mode,
completedEvent.mode2,
Config.punctuation,
Config.language,
Config.difficulty,
Config.lazyMode,
completedEvent.wpm,
completedEvent.acc,
completedEvent.rawWpm,
completedEvent.consistency
);
}
$("#retrySavingResultButton").addClass("hidden");
Notifications.add("Result saved", 1);
}
function buildCompletedEvent(difficultyFailed) {
@ -1467,70 +1458,62 @@ export async function finish(difficultyFailed = false) {
completedEvent.challenge = ChallengeContoller.verify(completedEvent);
if (!completedEvent.challenge) delete completedEvent.challenge;
completedEvent.hash = objecthash(completedEvent);
axiosInstance
.post("/results/add", {
result: completedEvent,
})
.then((response) => {
AccountButton.loading(false);
Result.hideCrown();
if (response.status !== 200) {
Notifications.add("Result not saved. " + response.data.message, -1);
} else {
completedEvent._id = response.data.insertedId;
if (response.data.isPb) {
completedEvent.isPb = true;
}
const response = await Ape.results.save(completedEvent);
DB.saveLocalResult(completedEvent);
DB.updateLocalStats({
time:
completedEvent.testDuration +
completedEvent.incompleteTestSeconds -
completedEvent.afkDuration,
started: TestStats.restartCount + 1,
});
AccountButton.loading(false);
try {
firebase.analytics().logEvent("testCompleted", completedEvent);
} catch (e) {
console.log("Analytics unavailable");
}
if (response.status !== 200) {
$("#retrySavingResultButton").removeClass("hidden");
if (response.message === "Incorrect result hash") {
console.log(completedEvent);
}
retrySaving.completedEvent = completedEvent;
retrySaving.canRetry = true;
return Notifications.add("Failed to save result: " + response.message, -1);
}
if (response.data.isPb) {
//new pb
Result.showCrown();
Result.updateCrown();
DB.saveLocalPB(
Config.mode,
completedEvent.mode2,
Config.punctuation,
Config.language,
Config.difficulty,
Config.lazyMode,
completedEvent.wpm,
completedEvent.acc,
completedEvent.rawWpm,
completedEvent.consistency
);
}
}
Result.hideCrown();
$("#retrySavingResultButton").addClass("hidden");
})
.catch((e) => {
AccountButton.loading(false);
let msg = e?.response?.data?.message ?? e.message;
Notifications.add("Failed to save result: " + msg, -1);
$("#retrySavingResultButton").removeClass("hidden");
if (msg == "Incorrect result hash") {
console.log(completedEvent);
}
completedEvent._id = response.data.insertedId;
if (response.data.isPb) {
completedEvent.isPb = true;
}
retrySaving.completedEvent = completedEvent;
retrySaving.canRetry = true;
});
DB.saveLocalResult(completedEvent);
DB.updateLocalStats({
time:
completedEvent.testDuration +
completedEvent.incompleteTestSeconds -
completedEvent.afkDuration,
started: TestStats.restartCount + 1,
});
try {
firebase.analytics().logEvent("testCompleted", completedEvent);
} catch (e) {
console.log("Analytics unavailable");
}
if (response.data.isPb) {
//new pb
Result.showCrown();
Result.updateCrown();
DB.saveLocalPB(
Config.mode,
completedEvent.mode2,
Config.punctuation,
Config.language,
Config.difficulty,
Config.lazyMode,
completedEvent.wpm,
completedEvent.acc,
completedEvent.rawWpm,
completedEvent.consistency
);
}
$("#retrySavingResultButton").addClass("hidden");
}
export function fail(reason) {

View file

@ -140,7 +140,7 @@ declare namespace MonkeyTypes {
interface Preset {
_id: string;
name: string;
config: PresetConfig;
config: ConfigChanges;
}
interface PersonalBest {
@ -316,6 +316,10 @@ declare namespace MonkeyTypes {
showAvg: boolean;
}
interface ConfigChanges extends Partial<MonkeyTypes.Config> {
tags?: string[];
}
interface DefaultConfig extends Config {
wordCount: WordsModes;
}

View file

@ -1,43 +1,13 @@
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Projects */
// "incremental": true, /* Enable incremental compilation */
// "composite": true, /* Enable constraints that allow a TypeScript project to be used with project references. */
// "tsBuildInfoFile": "./", /* Specify the folder for .tsbuildinfo incremental compilation files. */
// "disableSourceOfProjectReferenceRedirect": true, /* Disable preferring source files instead of declaration files when referencing composite projects */
// "disableSolutionSearching": true, /* Opt a project out of multi-project reference checking when editing. */
// "disableReferencedProjectLoad": true, /* Reduce the number of projects loaded automatically by TypeScript. */
/* Language and Environment */
"target": "es6" /* Set the JavaScript language version for emitted JavaScript and include compatible library declarations. */,
"lib": [
"ESNext",
"DOM",
"DOM.Iterable"
] /* Specify a set of bundled library declaration files that describe the target runtime environment. */,
// "jsx": "preserve", /* Specify what JSX code is generated. */
// "experimentalDecorators": true, /* Enable experimental support for TC39 stage 2 draft decorators. */
// "emitDecoratorMetadata": true, /* Emit design-type metadata for decorated declarations in source files. */
// "jsxFactory": "", /* Specify the JSX factory function used when targeting React JSX emit, e.g. 'React.createElement' or 'h' */
// "jsxFragmentFactory": "", /* Specify the JSX Fragment reference used for fragments when targeting React JSX emit e.g. 'React.Fragment' or 'Fragment'. */
// "jsxImportSource": "", /* Specify module specifier used to import the JSX factory functions when using `jsx: react-jsx*`.` */
// "reactNamespace": "", /* Specify the object invoked for `createElement`. This only applies when targeting `react` JSX emit. */
// "noLib": true, /* Disable including any library files, including the default lib.d.ts. */
// "useDefineForClassFields": true, /* Emit ECMAScript-standard-compliant class fields. */
/* Modules */
"module": "ES6" /* Specify what module code is generated. */,
// "rootDir": "./src/js" /* Specify the root folder within your source files. */,
"moduleResolution": "node" /* Specify how TypeScript looks up a file from a given module specifier. */,
// "baseUrl": "./", /* Specify the base directory to resolve non-relative module names. */
// "paths": {}, /* Specify a set of entries that re-map imports to additional lookup locations. */
// "rootDirs": [], /* Allow multiple folders to be treated as one when resolving modules. */
// "typeRoots": [
// "./src/js/Types",
// "./node_modules/@types"
// ] /* Specify multiple folders that act like `./node_modules/@types`. */,
"types": [
"moment",
"jquery",
@ -46,45 +16,11 @@
] /* Specify type package names to be included without being referenced in a source file. */,
"allowUmdGlobalAccess": true /* Allow accessing UMD globals from modules. */,
"resolveJsonModule": true /* Enable importing .json files */,
// "noResolve": true, /* Disallow `import`s, `require`s or `<reference>`s from expanding the number of files TypeScript should add to a project. */
/* JavaScript Support */
"allowJs": true /* Allow JavaScript files to be a part of your program. Use the `checkJS` option to get errors from these files. */,
// "checkJs": true, /* Enable error reporting in type-checked JavaScript files. */
// "maxNodeModuleJsDepth": 1, /* Specify the maximum folder depth used for checking JavaScript files from `node_modules`. Only applicable with `allowJs`. */
/* Emit */
// "declaration": true, /* Generate .d.ts files from TypeScript and JavaScript files in your project. */
// "declarationMap": true, /* Create sourcemaps for d.ts files. */
// "emitDeclarationOnly": true, /* Only output d.ts files and not JavaScript files. */
// "sourceMap": true, /* Create source map files for emitted JavaScript files. */
// "outFile": "./", /* Specify a file that bundles all outputs into one JavaScript file. If `declaration` is true, also designates a file that bundles all .d.ts output. */
"outDir": "./built" /* Specify an output folder for all emitted files. */,
// "removeComments": true, /* Disable emitting comments. */
// "noEmit": true, /* Disable emitting files from a compilation. */
// "importHelpers": true, /* Allow importing helper functions from tslib once per project, instead of including them per-file. */
// "importsNotUsedAsValues": "remove", /* Specify emit/checking behavior for imports that are only used for types */
// "downlevelIteration": true, /* Emit more compliant, but verbose and less performant JavaScript for iteration. */
// "sourceRoot": "", /* Specify the root path for debuggers to find the reference source code. */
// "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */
// "inlineSourceMap": true, /* Include sourcemap files inside the emitted JavaScript. */
// "inlineSources": true, /* Include source code in the sourcemaps inside the emitted JavaScript. */
// "emitBOM": true, /* Emit a UTF-8 Byte Order Mark (BOM) in the beginning of output files. */
"outDir": "./build" /* Specify an output folder for all emitted files. */,
"newLine": "lf" /* Set the newline character for emitting files. */,
// "stripInternal": true, /* Disable emitting declarations that have `@internal` in their JSDoc comments. */
// "noEmitHelpers": true, /* Disable generating custom helper functions like `__extends` in compiled output. */
// "noEmitOnError": true, /* Disable emitting files if any type checking errors are reported. */
// "preserveConstEnums": true, /* Disable erasing `const enum` declarations in generated code. */
// "declarationDir": "./", /* Specify the output directory for generated declaration files. */
/* Interop Constraints */
// "isolatedModules": true, /* Ensure that each file can be safely transpiled without relying on other imports. */
// "allowSyntheticDefaultImports": true, /* Allow 'import x from y' when a module doesn't have a default export. */
"esModuleInterop": true /* Emit additional JavaScript to ease support for importing CommonJS modules. This enables `allowSyntheticDefaultImports` for type compatibility. */,
// "preserveSymlinks": true, /* Disable resolving symlinks to their realpath. This correlates to the same flag in node. */
"forceConsistentCasingInFileNames": true /* Ensure that casing is correct in imports. */,
/* Type Checking */
"strict": true /* Enable all strict type-checking options. */,
"noImplicitAny": true /* Enable error reporting for expressions and declarations with an implied `any` type.. */,
"strictNullChecks": true /* When type checking, take into account `null` and `undefined`. */,

7
package-lock.json generated
View file

@ -25,7 +25,8 @@
"jsonschema": "1.4.0",
"prettier": "2.5.1",
"pretty-quick": "3.1.0",
"run-script-os": "1.1.6"
"run-script-os": "1.1.6",
"typescript": "^4.5.5"
},
"engines": {
"npm": "8.1.2"
@ -5340,7 +5341,6 @@
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
"dev": true,
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@ -9886,8 +9886,7 @@
"version": "4.5.5",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-4.5.5.tgz",
"integrity": "sha512-TCTIul70LyWe6IJWT8QSYeA54WQe8EjQFU4wY52Fasj5UKx88LNYKCgBEHcOMOrFF1rKGbD8v/xcNWVUq9SymA==",
"dev": true,
"peer": true
"dev": true
},
"unbox-primitive": {
"version": "1.0.1",

View file

@ -12,9 +12,12 @@
"install:windows": ".\\bin\\install.cmd",
"lint": "run-script-os",
"lint:windows": ".\\node_modules\\.bin\\eslint \"./backend/**/*.{ts,js}\" \"./frontend/src/scripts/**/*.{ts,js}\"",
"lint:default": "./node_modules/.bin/eslint './backend/**/*.{ts,js}' './frontend/src/scripts/**/*.{ts,js}'",
"lint:default": "eslint --ignore-pattern '**/build/**' './backend/**/*.js' './frontend/src/scripts/**/*.js'",
"build:live": "cd ./frontend && npm run build:live",
"pretty": "prettier --check './backend/**/*.js' './frontend/src/**/*.{js,scss}' './frontend/static/**/*.{json,html}'",
"start:dev-ts": "concurrently --kill-others \"cd ./frontend && npm run start:dev-ts\" \"cd ./backend && npm run start:dev\"",
"start:dev:fe-ts": "cd frontend && npm run start:dev-ts",
"deploy:live-ts": "cd frontend && npm run deploy:live-ts",
"pr-check-lint-json": "cd frontend && npx gulp pr-check-lint-json",
"pr-check-quote-json": "cd frontend && npx gulp pr-check-quote-json",
"pr-check-language-json": "cd frontend && npx gulp pr-check-language-json",
@ -42,7 +45,8 @@
"jsonschema": "1.4.0",
"prettier": "2.5.1",
"pretty-quick": "3.1.0",
"run-script-os": "1.1.6"
"run-script-os": "1.1.6",
"typescript": "^4.5.5"
},
"husky": {
"hooks": {