mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2024-09-20 07:16:17 +08:00
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:
parent
9eb4e6ac86
commit
f9d6f52c15
2
.gitignore
vendored
2
.gitignore
vendored
|
@ -102,4 +102,4 @@ backend/anticheat
|
|||
dep-graph.png
|
||||
|
||||
# TypeScript
|
||||
built/
|
||||
build/
|
|
@ -7,3 +7,4 @@ sound/*
|
|||
node_modules
|
||||
css/balloon.css
|
||||
_list.json
|
||||
backend/build
|
||||
|
|
1
backend/.gitignore
vendored
1
backend/.gitignore
vendored
|
@ -1,3 +1,4 @@
|
|||
lastId.txt
|
||||
log_success.txt
|
||||
log_failed.txt
|
||||
build
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
29
backend/api/routes/configs.js
Normal file
29
backend/api/routes/configs.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
|
@ -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;
|
10
backend/api/routes/psas.js
Normal file
10
backend/api/routes/psas.js
Normal 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;
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
50
backend/api/routes/results.js
Normal file
50
backend/api/routes/results.js
Normal 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;
|
|
@ -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;
|
|
@ -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;
|
||||
|
|
|
@ -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
27
backend/app.js
Normal 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();
|
|
@ -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);
|
|
@ -34,4 +34,4 @@ const SUPPORTED_QUOTE_LANGUAGES = [
|
|||
"vietnamese",
|
||||
];
|
||||
|
||||
module.exports = SUPPORTED_QUOTE_LANGUAGES;
|
||||
export default SUPPORTED_QUOTE_LANGUAGES;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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))
|
||||
);
|
||||
}
|
||||
|
|
23
backend/handlers/monkey-response.js
Normal file
23
backend/handlers/monkey-response.js
Normal 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 });
|
||||
}
|
|
@ -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,
|
||||
};
|
||||
},
|
||||
};
|
||||
};
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -1,7 +1,5 @@
|
|||
const db = require("./db");
|
||||
import db from "./db";
|
||||
|
||||
module.exports = {
|
||||
mongoDB() {
|
||||
return db;
|
||||
},
|
||||
};
|
||||
export function mongoDB() {
|
||||
return db;
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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];
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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,
|
||||
};
|
113
backend/middlewares/api-utils.ts
Normal file
113
backend/middlewares/api-utils.ts
Normal 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 };
|
|
@ -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 };
|
||||
|
|
|
@ -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;
|
||||
|
|
58
backend/middlewares/error.js
Normal file
58
backend/middlewares/error.js
Normal 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;
|
|
@ -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,
|
||||
});
|
277
backend/middlewares/rate-limit.ts
Normal file
277
backend/middlewares/rate-limit.ts
Normal 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,
|
||||
});
|
62
backend/package-lock.json
generated
62
backend/package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
43
backend/server.ts
Normal 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
19
backend/tsconfig.json
Normal 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
5
backend/types/anticheat.d.ts
vendored
Normal 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
32
backend/types/types.d.ts
vendored
Normal 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;
|
||||
}
|
||||
}
|
|
@ -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();
|
18
frontend/src/scripts/ape/endpoints/configs.ts
Normal file
18
frontend/src/scripts/ape/endpoints/configs.ts
Normal 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,
|
||||
};
|
||||
}
|
17
frontend/src/scripts/ape/endpoints/index.ts
Normal file
17
frontend/src/scripts/ape/endpoints/index.ts
Normal 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,
|
||||
};
|
39
frontend/src/scripts/ape/endpoints/leaderboards.ts
Normal file
39
frontend/src/scripts/ape/endpoints/leaderboards.ts
Normal 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 };
|
||||
}
|
41
frontend/src/scripts/ape/endpoints/presets.ts
Normal file
41
frontend/src/scripts/ape/endpoints/presets.ts
Normal 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 };
|
||||
}
|
11
frontend/src/scripts/ape/endpoints/psas.ts
Normal file
11
frontend/src/scripts/ape/endpoints/psas.ts
Normal 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 };
|
||||
}
|
95
frontend/src/scripts/ape/endpoints/quotes.ts
Normal file
95
frontend/src/scripts/ape/endpoints/quotes.ts
Normal 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,
|
||||
};
|
||||
}
|
30
frontend/src/scripts/ape/endpoints/results.ts
Normal file
30
frontend/src/scripts/ape/endpoints/results.ts
Normal 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 };
|
||||
}
|
124
frontend/src/scripts/ape/endpoints/users.ts
Normal file
124
frontend/src/scripts/ape/endpoints/users.ts
Normal 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,
|
||||
};
|
||||
}
|
123
frontend/src/scripts/ape/index.ts
Normal file
123
frontend/src/scripts/ape/index.ts
Normal 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
134
frontend/src/scripts/ape/types/ape.d.ts
vendored
Normal 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;
|
||||
};
|
||||
}
|
||||
}
|
|
@ -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;
|
|
@ -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) {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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) {
|
||||
|
|
|
@ -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> {
|
||||
|
|
|
@ -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();
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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) => {
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
);
|
||||
}
|
||||
});
|
||||
|
|
|
@ -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();
|
||||
},
|
||||
() => {
|
||||
//
|
||||
|
|
|
@ -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) {
|
||||
|
|
6
frontend/src/scripts/types/types.d.ts
vendored
6
frontend/src/scripts/types/types.d.ts
vendored
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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
7
package-lock.json
generated
|
@ -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",
|
||||
|
|
|
@ -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": {
|
||||
|
|
Loading…
Reference in a new issue