From f9d6f52c1536774a77d707219836dbd1aa80fdc7 Mon Sep 17 00:00:00 2001 From: Bruce Berrios <58147810+Bruception@users.noreply.github.com> Date: Tue, 22 Feb 2022 14:55:48 -0500 Subject: [PATCH] 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 * 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 * Fix typescript compilation errors Signed-off-by: Brian Evans * Migrated backend to ES modules Switched to import export syntax Signed-off-by: Brian Evans * Add typescript declaration for anticheat Signed-off-by: Brian Evans * Rename top level files to .ts Fix service account json file typing Signed-off-by: Brian Evans * Add dev build scripts for backend typescript Signed-off-by: Brian Evans * 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 * Fixed backend commonjs syntax to ES module syntax Signed-off-by: Brian Evans * Add build to backend start script Signed-off-by: Brian Evans * 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 Co-authored-by: Brian Evans <53117772+mrbrianevans@users.noreply.github.com> Co-authored-by: Miodec --- .gitignore | 2 +- .prettierignore | 1 + backend/.gitignore | 1 + backend/api/controllers/config.js | 15 +- backend/api/controllers/leaderboards.js | 21 +- backend/api/controllers/new-quotes.js | 36 +-- backend/api/controllers/preset.js | 24 +- backend/api/controllers/psa.js | 9 +- backend/api/controllers/quote-ratings.js | 17 +- backend/api/controllers/quotes.js | 23 +- backend/api/controllers/result.js | 73 +++-- backend/api/controllers/user.js | 112 +++---- backend/api/routes/config.js | 32 -- backend/api/routes/configs.js | 29 ++ backend/api/routes/index.js | 52 +++- backend/api/routes/leaderboards.js | 20 +- backend/api/routes/{preset.js => presets.js} | 34 +-- backend/api/routes/psa.js | 11 - backend/api/routes/psas.js | 10 + backend/api/routes/quotes.js | 28 +- backend/api/routes/result.js | 53 ---- backend/api/routes/results.js | 50 ++++ backend/api/routes/{user.js => users.js} | 20 +- backend/api/schemas/config-schema.js | 6 +- backend/api/schemas/result-schema.js | 4 +- backend/app.js | 27 ++ ...configuration.js => base-configuration.ts} | 9 +- backend/constants/quote-languages.js | 2 +- backend/dao/bot.js | 10 +- backend/dao/config.js | 8 +- backend/dao/configuration.js | 10 +- backend/dao/leaderboards.js | 10 +- backend/dao/new-quotes.js | 40 +-- backend/dao/preset.js | 25 +- backend/dao/psa.js | 6 +- backend/dao/public-stats.js | 28 +- backend/dao/quote-ratings.js | 14 +- backend/dao/report.js | 10 +- backend/dao/result.js | 28 +- backend/dao/user.js | 170 +++++------ backend/handlers/auth.js | 23 +- backend/handlers/captcha.js | 34 +-- backend/handlers/error.js | 16 +- backend/handlers/logger.js | 26 +- backend/handlers/misc.js | 50 ++-- backend/handlers/monkey-response.js | 23 ++ backend/handlers/pb.js | 210 +++++++------ backend/handlers/validation.js | 17 +- backend/init/db.js | 4 +- backend/init/mongodb.js | 10 +- backend/jobs/delete-old-logs.js | 10 +- backend/jobs/index.js | 6 +- backend/jobs/update-leaderboards.js | 8 +- backend/middlewares/api-utils.js | 94 ------ backend/middlewares/api-utils.ts | 113 +++++++ backend/middlewares/auth.js | 30 +- backend/middlewares/context.js | 6 +- backend/middlewares/error.js | 58 ++++ backend/middlewares/rate-limit.js | 263 ----------------- backend/middlewares/rate-limit.ts | 277 ++++++++++++++++++ backend/package-lock.json | 62 +++- backend/package.json | 14 +- backend/server.js | 94 ------ backend/server.ts | 43 +++ backend/tsconfig.json | 19 ++ backend/types/anticheat.d.ts | 5 + backend/types/types.d.ts | 32 ++ backend/{worker.js => worker.ts} | 17 +- frontend/src/scripts/ape/endpoints/configs.ts | 18 ++ frontend/src/scripts/ape/endpoints/index.ts | 17 ++ .../src/scripts/ape/endpoints/leaderboards.ts | 39 +++ frontend/src/scripts/ape/endpoints/presets.ts | 41 +++ frontend/src/scripts/ape/endpoints/psas.ts | 11 + frontend/src/scripts/ape/endpoints/quotes.ts | 95 ++++++ frontend/src/scripts/ape/endpoints/results.ts | 30 ++ frontend/src/scripts/ape/endpoints/users.ts | 124 ++++++++ frontend/src/scripts/ape/index.ts | 123 ++++++++ frontend/src/scripts/ape/types/ape.d.ts | 134 +++++++++ frontend/src/scripts/axios-instance.ts | 60 ---- .../scripts/controllers/account-controller.js | 188 +++++------- .../controllers/verification-controller.ts | 35 +-- frontend/src/scripts/db.ts | 131 +++------ frontend/src/scripts/elements/leaderboards.ts | 180 +++++------- .../src/scripts/elements/notifications.ts | 2 +- frontend/src/scripts/elements/psa.ts | 6 +- .../src/scripts/popups/edit-preset-popup.ts | 127 +++----- .../src/scripts/popups/edit-tags-popup.ts | 90 ++---- .../src/scripts/popups/quote-approve-popup.ts | 134 ++++----- .../src/scripts/popups/quote-rate-popup.ts | 175 ++++++----- .../src/scripts/popups/quote-report-popup.ts | 61 ++-- .../src/scripts/popups/quote-submit-popup.ts | 48 ++- .../src/scripts/popups/result-tags-popup.ts | 130 ++++---- frontend/src/scripts/popups/simple-popups.ts | 251 +++++++--------- frontend/src/scripts/test/test-logic.js | 213 +++++++------- frontend/src/scripts/types/types.d.ts | 6 +- frontend/tsconfig.json | 66 +---- package-lock.json | 7 +- package.json | 8 +- 98 files changed, 2777 insertions(+), 2417 deletions(-) delete mode 100644 backend/api/routes/config.js create mode 100644 backend/api/routes/configs.js rename backend/api/routes/{preset.js => presets.js} (67%) delete mode 100644 backend/api/routes/psa.js create mode 100644 backend/api/routes/psas.js delete mode 100644 backend/api/routes/result.js create mode 100644 backend/api/routes/results.js rename backend/api/routes/{user.js => users.js} (89%) create mode 100644 backend/app.js rename backend/constants/{base-configuration.js => base-configuration.ts} (75%) create mode 100644 backend/handlers/monkey-response.js delete mode 100644 backend/middlewares/api-utils.js create mode 100644 backend/middlewares/api-utils.ts create mode 100644 backend/middlewares/error.js delete mode 100644 backend/middlewares/rate-limit.js create mode 100644 backend/middlewares/rate-limit.ts delete mode 100644 backend/server.js create mode 100644 backend/server.ts create mode 100644 backend/tsconfig.json create mode 100644 backend/types/anticheat.d.ts create mode 100644 backend/types/types.d.ts rename backend/{worker.js => worker.ts} (84%) create mode 100644 frontend/src/scripts/ape/endpoints/configs.ts create mode 100644 frontend/src/scripts/ape/endpoints/index.ts create mode 100644 frontend/src/scripts/ape/endpoints/leaderboards.ts create mode 100644 frontend/src/scripts/ape/endpoints/presets.ts create mode 100644 frontend/src/scripts/ape/endpoints/psas.ts create mode 100644 frontend/src/scripts/ape/endpoints/quotes.ts create mode 100644 frontend/src/scripts/ape/endpoints/results.ts create mode 100644 frontend/src/scripts/ape/endpoints/users.ts create mode 100644 frontend/src/scripts/ape/index.ts create mode 100644 frontend/src/scripts/ape/types/ape.d.ts delete mode 100644 frontend/src/scripts/axios-instance.ts diff --git a/.gitignore b/.gitignore index 03ab6ffc0..44de0024d 100644 --- a/.gitignore +++ b/.gitignore @@ -102,4 +102,4 @@ backend/anticheat dep-graph.png # TypeScript -built/ \ No newline at end of file +build/ \ No newline at end of file diff --git a/.prettierignore b/.prettierignore index 1a097dea5..06fef89d3 100644 --- a/.prettierignore +++ b/.prettierignore @@ -7,3 +7,4 @@ sound/* node_modules css/balloon.css _list.json +backend/build diff --git a/backend/.gitignore b/backend/.gitignore index 829210d34..18a3398d4 100644 --- a/backend/.gitignore +++ b/backend/.gitignore @@ -1,3 +1,4 @@ lastId.txt log_success.txt log_failed.txt +build diff --git a/backend/api/controllers/config.js b/backend/api/controllers/config.js index 6f4114eaf..6e2bc95f0 100644 --- a/backend/api/controllers/config.js +++ b/backend/api/controllers/config.js @@ -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; diff --git a/backend/api/controllers/leaderboards.js b/backend/api/controllers/leaderboards.js index 8e31f9f48..06166e58c 100644 --- a/backend/api/controllers/leaderboards.js +++ b/backend/api/controllers/leaderboards.js @@ -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; diff --git a/backend/api/controllers/new-quotes.js b/backend/api/controllers/new-quotes.js index 9892d75c4..89a29a56b 100644 --- a/backend/api/controllers/new-quotes.js +++ b/backend/api/controllers/new-quotes.js @@ -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; diff --git a/backend/api/controllers/preset.js b/backend/api/controllers/preset.js index 5ca789beb..c3c614732 100644 --- a/backend/api/controllers/preset.js +++ b/backend/api/controllers/preset.js @@ -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; diff --git a/backend/api/controllers/psa.js b/backend/api/controllers/psa.js index 8cab7bdd9..2dad56e07 100644 --- a/backend/api/controllers/psa.js +++ b/backend/api/controllers/psa.js @@ -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; diff --git a/backend/api/controllers/quote-ratings.js b/backend/api/controllers/quote-ratings.js index 534ec84e5..bcd2604f4 100644 --- a/backend/api/controllers/quote-ratings.js +++ b/backend/api/controllers/quote-ratings.js @@ -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; diff --git a/backend/api/controllers/quotes.js b/backend/api/controllers/quotes.js index 76cd20b7d..f30e7e3d7 100644 --- a/backend/api/controllers/quotes.js +++ b/backend/api/controllers/quotes.js @@ -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; diff --git a/backend/api/controllers/result.js b/backend/api/controllers/result.js index 56a52173f..53a696ef2 100644 --- a/backend/api/controllers/result.js +++ b/backend/api/controllers/result.js @@ -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; diff --git a/backend/api/controllers/user.js b/backend/api/controllers/user.js index df1806269..0001d1085 100644 --- a/backend/api/controllers/user.js +++ b/backend/api/controllers/user.js @@ -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; diff --git a/backend/api/routes/config.js b/backend/api/routes/config.js deleted file mode 100644 index e5ea8794b..000000000 --- a/backend/api/routes/config.js +++ /dev/null @@ -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; diff --git a/backend/api/routes/configs.js b/backend/api/routes/configs.js new file mode 100644 index 000000000..736ba2c5e --- /dev/null +++ b/backend/api/routes/configs.js @@ -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; diff --git a/backend/api/routes/index.js b/backend/api/routes/index.js index 2f227245d..f39b2396f 100644 --- a/backend/api/routes/index.js +++ b/backend/api/routes/index.js @@ -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; diff --git a/backend/api/routes/leaderboards.js b/backend/api/routes/leaderboards.js index c0972be2c..cf702e8c0 100644 --- a/backend/api/routes/leaderboards.js +++ b/backend/api/routes/leaderboards.js @@ -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; diff --git a/backend/api/routes/preset.js b/backend/api/routes/presets.js similarity index 67% rename from backend/api/routes/preset.js rename to backend/api/routes/presets.js index aae1ed08b..508867c12 100644 --- a/backend/api/routes/preset.js +++ b/backend/api/routes/presets.js @@ -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; diff --git a/backend/api/routes/psa.js b/backend/api/routes/psa.js deleted file mode 100644 index 56a43d5d2..000000000 --- a/backend/api/routes/psa.js +++ /dev/null @@ -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; diff --git a/backend/api/routes/psas.js b/backend/api/routes/psas.js new file mode 100644 index 000000000..c00f7062e --- /dev/null +++ b/backend/api/routes/psas.js @@ -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; diff --git a/backend/api/routes/quotes.js b/backend/api/routes/quotes.js index 15db94f17..5598f7a49 100644 --- a/backend/api/routes/quotes.js +++ b/backend/api/routes/quotes.js @@ -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; diff --git a/backend/api/routes/result.js b/backend/api/routes/result.js deleted file mode 100644 index 365f78ba3..000000000 --- a/backend/api/routes/result.js +++ /dev/null @@ -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; diff --git a/backend/api/routes/results.js b/backend/api/routes/results.js new file mode 100644 index 000000000..9a9b02866 --- /dev/null +++ b/backend/api/routes/results.js @@ -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; diff --git a/backend/api/routes/user.js b/backend/api/routes/users.js similarity index 89% rename from backend/api/routes/user.js rename to backend/api/routes/users.js index 78f5d7481..3cdded2a3 100644 --- a/backend/api/routes/user.js +++ b/backend/api/routes/users.js @@ -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; diff --git a/backend/api/schemas/config-schema.js b/backend/api/schemas/config-schema.js index c205da607..05c376251 100644 --- a/backend/api/schemas/config-schema.js +++ b/backend/api/schemas/config-schema.js @@ -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; diff --git a/backend/api/schemas/result-schema.js b/backend/api/schemas/result-schema.js index 8164d3974..039c0e504 100644 --- a/backend/api/schemas/result-schema.js +++ b/backend/api/schemas/result-schema.js @@ -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; diff --git a/backend/app.js b/backend/app.js new file mode 100644 index 000000000..e1cbfef90 --- /dev/null +++ b/backend/app.js @@ -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(); diff --git a/backend/constants/base-configuration.js b/backend/constants/base-configuration.ts similarity index 75% rename from backend/constants/base-configuration.js rename to backend/constants/base-configuration.ts index 977578588..6d74ef373 100644 --- a/backend/constants/base-configuration.js +++ b/backend/constants/base-configuration.ts @@ -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); diff --git a/backend/constants/quote-languages.js b/backend/constants/quote-languages.js index afb7cedec..a58356ae5 100644 --- a/backend/constants/quote-languages.js +++ b/backend/constants/quote-languages.js @@ -34,4 +34,4 @@ const SUPPORTED_QUOTE_LANGUAGES = [ "vietnamese", ]; -module.exports = SUPPORTED_QUOTE_LANGUAGES; +export default SUPPORTED_QUOTE_LANGUAGES; diff --git a/backend/dao/bot.js b/backend/dao/bot.js index 05fddb3af..5fc9b7fc7 100644 --- a/backend/dao/bot.js +++ b/backend/dao/bot.js @@ -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; diff --git a/backend/dao/config.js b/backend/dao/config.js index c77a1381c..07f6b0c9f 100644 --- a/backend/dao/config.js +++ b/backend/dao/config.js @@ -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; diff --git a/backend/dao/configuration.js b/backend/dao/configuration.js index 3f887f00c..7847734eb 100644 --- a/backend/dao/configuration.js +++ b/backend/dao/configuration.js @@ -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; diff --git a/backend/dao/leaderboards.js b/backend/dao/leaderboards.js index 31a621055..f7c9dd18a 100644 --- a/backend/dao/leaderboards.js +++ b/backend/dao/leaderboards.js @@ -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; diff --git a/backend/dao/new-quotes.js b/backend/dao/new-quotes.js index ae032a4eb..f064fb22a 100644 --- a/backend/dao/new-quotes.js +++ b/backend/dao/new-quotes.js @@ -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; diff --git a/backend/dao/preset.js b/backend/dao/preset.js index 37a51e5f0..191b1aec5 100644 --- a/backend/dao/preset.js +++ b/backend/dao/preset.js @@ -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; diff --git a/backend/dao/psa.js b/backend/dao/psa.js index f1c028595..364da12d9 100644 --- a/backend/dao/psa.js +++ b/backend/dao/psa.js @@ -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; diff --git a/backend/dao/public-stats.js b/backend/dao/public-stats.js index 368b3915f..1fd552df3 100644 --- a/backend/dao/public-stats.js +++ b/backend/dao/public-stats.js @@ -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; diff --git a/backend/dao/quote-ratings.js b/backend/dao/quote-ratings.js index 1540cc3f5..070aea937 100644 --- a/backend/dao/quote-ratings.js +++ b/backend/dao/quote-ratings.js @@ -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; diff --git a/backend/dao/report.js b/backend/dao/report.js index c75a409cd..63501ae9f 100644 --- a/backend/dao/report.js +++ b/backend/dao/report.js @@ -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; diff --git a/backend/dao/result.js b/backend/dao/result.js index ff3eeae08..f4b6f3d07 100644 --- a/backend/dao/result.js +++ b/backend/dao/result.js @@ -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; diff --git a/backend/dao/user.js b/backend/dao/user.js index 65bc3b4a0..661d853a7 100644 --- a/backend/dao/user.js +++ b/backend/dao/user.js @@ -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; diff --git a/backend/handlers/auth.js b/backend/handlers/auth.js index 466650099..03dd58992 100644 --- a/backend/handlers/auth.js +++ b/backend/handlers/auth.js @@ -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, + }); +} diff --git a/backend/handlers/captcha.js b/backend/handlers/captcha.js index 207cdf6fa..c50a9565b 100644 --- a/backend/handlers/captcha.js +++ b/backend/handlers/captcha.js @@ -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; +} diff --git a/backend/handlers/error.js b/backend/handlers/error.js index 4f570a0e1..086ed826d 100644 --- a/backend/handlers/error.js +++ b/backend/handlers/error.js @@ -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; diff --git a/backend/handlers/logger.js b/backend/handlers/logger.js index 350724639..dcfa94ded 100644 --- a/backend/handlers/logger.js +++ b/backend/handlers/logger.js @@ -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, + }); + }, }; diff --git a/backend/handlers/misc.js b/backend/handlers/misc.js index 5737b8533..ae599ad7c 100644 --- a/backend/handlers/misc.js +++ b/backend/handlers/misc.js @@ -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)) + ); +} diff --git a/backend/handlers/monkey-response.js b/backend/handlers/monkey-response.js new file mode 100644 index 000000000..f422524d7 --- /dev/null +++ b/backend/handlers/monkey-response.js @@ -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 }); +} diff --git a/backend/handlers/pb.js b/backend/handlers/pb.js index fe344f310..b54bc73d1 100644 --- a/backend/handlers/pb.js +++ b/backend/handlers/pb.js @@ -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, - }; - }, -}; + }; +} diff --git a/backend/handlers/validation.js b/backend/handlers/validation.js index 1eea67ab7..c558abc78 100644 --- a/backend/handlers/validation.js +++ b/backend/handlers/validation.js @@ -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, -}; diff --git a/backend/init/db.js b/backend/init/db.js index 8a6a48ade..e5a73107a 100644 --- a/backend/init/db.js +++ b/backend/init/db.js @@ -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; diff --git a/backend/init/mongodb.js b/backend/init/mongodb.js index 5bd5f6e4b..4b0f51b5c 100644 --- a/backend/init/mongodb.js +++ b/backend/init/mongodb.js @@ -1,7 +1,5 @@ -const db = require("./db"); +import db from "./db"; -module.exports = { - mongoDB() { - return db; - }, -}; +export function mongoDB() { + return db; +} diff --git a/backend/jobs/delete-old-logs.js b/backend/jobs/delete-old-logs.js index 34f6afc1a..eb31b350d 100644 --- a/backend/jobs/delete-old-logs.js +++ b/backend/jobs/delete-old-logs.js @@ -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); diff --git a/backend/jobs/index.js b/backend/jobs/index.js index 2124f0f34..803c614be 100644 --- a/backend/jobs/index.js +++ b/backend/jobs/index.js @@ -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]; diff --git a/backend/jobs/update-leaderboards.js b/backend/jobs/update-leaderboards.js index c545988e0..888203bc3 100644 --- a/backend/jobs/update-leaderboards.js +++ b/backend/jobs/update-leaderboards.js @@ -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); diff --git a/backend/middlewares/api-utils.js b/backend/middlewares/api-utils.js deleted file mode 100644 index 8f6405cb6..000000000 --- a/backend/middlewares/api-utils.js +++ /dev/null @@ -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, -}; diff --git a/backend/middlewares/api-utils.ts b/backend/middlewares/api-utils.ts new file mode 100644 index 000000000..39a6c300c --- /dev/null +++ b/backend/middlewares/api-utils.ts @@ -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; + +/** + * 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 }; diff --git a/backend/middlewares/auth.js b/backend/middlewares/auth.js index 9d99f783a..7147edb33 100644 --- a/backend/middlewares/auth.js +++ b/backend/middlewares/auth.js @@ -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 }; diff --git a/backend/middlewares/context.js b/backend/middlewares/context.js index 496fc828f..b07e9e3e7 100644 --- a/backend/middlewares/context.js +++ b/backend/middlewares/context.js @@ -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; diff --git a/backend/middlewares/error.js b/backend/middlewares/error.js new file mode 100644 index 000000000..8644c0921 --- /dev/null +++ b/backend/middlewares/error.js @@ -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; diff --git a/backend/middlewares/rate-limit.js b/backend/middlewares/rate-limit.js deleted file mode 100644 index 34d210532..000000000 --- a/backend/middlewares/rate-limit.js +++ /dev/null @@ -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, -}); diff --git a/backend/middlewares/rate-limit.ts b/backend/middlewares/rate-limit.ts new file mode 100644 index 000000000..470fa0ea0 --- /dev/null +++ b/backend/middlewares/rate-limit.ts @@ -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, +}); diff --git a/backend/package-lock.json b/backend/package-lock.json index 7bdc9bc4c..caccf9a96 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -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", diff --git a/backend/package.json b/backend/package.json index fec434191..d522eba02 100644 --- a/backend/package.json +++ b/backend/package.json @@ -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" } } diff --git a/backend/server.js b/backend/server.js deleted file mode 100644 index 1f301c388..000000000 --- a/backend/server.js +++ /dev/null @@ -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()); -}); diff --git a/backend/server.ts b/backend/server.ts new file mode 100644 index 000000000..b35c08d5b --- /dev/null +++ b/backend/server.ts @@ -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); diff --git a/backend/tsconfig.json b/backend/tsconfig.json new file mode 100644 index 000000000..92ba0d81d --- /dev/null +++ b/backend/tsconfig.json @@ -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"] +} diff --git a/backend/types/anticheat.d.ts b/backend/types/anticheat.d.ts new file mode 100644 index 000000000..37860d4ae --- /dev/null +++ b/backend/types/anticheat.d.ts @@ -0,0 +1,5 @@ +declare module "anticheat" { + function validateResult(result): string; + + function validateKeys(result, uid): string; +} diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts new file mode 100644 index 000000000..2fdabba0c --- /dev/null +++ b/backend/types/types.d.ts @@ -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; + } +} diff --git a/backend/worker.js b/backend/worker.ts similarity index 84% rename from backend/worker.js rename to backend/worker.ts index 8f77add63..a2e78091b 100644 --- a/backend/worker.js +++ b/backend/worker.ts @@ -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(); diff --git a/frontend/src/scripts/ape/endpoints/configs.ts b/frontend/src/scripts/ape/endpoints/configs.ts new file mode 100644 index 000000000..d9ebe013e --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/configs.ts @@ -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, + }; +} diff --git a/frontend/src/scripts/ape/endpoints/index.ts b/frontend/src/scripts/ape/endpoints/index.ts new file mode 100644 index 000000000..07dc9ddb4 --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/index.ts @@ -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, +}; diff --git a/frontend/src/scripts/ape/endpoints/leaderboards.ts b/frontend/src/scripts/ape/endpoints/leaderboards.ts new file mode 100644 index 000000000..076d6e299 --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/leaderboards.ts @@ -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 }; +} diff --git a/frontend/src/scripts/ape/endpoints/presets.ts b/frontend/src/scripts/ape/endpoints/presets.ts new file mode 100644 index 000000000..a5b74f0b4 --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/presets.ts @@ -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 }; +} diff --git a/frontend/src/scripts/ape/endpoints/psas.ts b/frontend/src/scripts/ape/endpoints/psas.ts new file mode 100644 index 000000000..2194d03cb --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/psas.ts @@ -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 }; +} diff --git a/frontend/src/scripts/ape/endpoints/quotes.ts b/frontend/src/scripts/ape/endpoints/quotes.ts new file mode 100644 index 000000000..29f6173e8 --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/quotes.ts @@ -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, + }; +} diff --git a/frontend/src/scripts/ape/endpoints/results.ts b/frontend/src/scripts/ape/endpoints/results.ts new file mode 100644 index 000000000..47dedaa00 --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/results.ts @@ -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 + ): 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 }; +} diff --git a/frontend/src/scripts/ape/endpoints/users.ts b/frontend/src/scripts/ape/endpoints/users.ts new file mode 100644 index 000000000..8a5026558 --- /dev/null +++ b/frontend/src/scripts/ape/endpoints/users.ts @@ -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, + 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, + }; +} diff --git a/frontend/src/scripts/ape/index.ts b/frontend/src/scripts/ape/index.ts new file mode 100644 index 000000000..7b591382f --- /dev/null +++ b/frontend/src/scripts/ape/index.ts @@ -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 { + 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; + +type AxiosClientDataMethod = ( + endpoint: string, + data: any, + config: AxiosRequestConfig +) => Promise; + +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; diff --git a/frontend/src/scripts/ape/types/ape.d.ts b/frontend/src/scripts/ape/types/ape.d.ts new file mode 100644 index 000000000..ea118af2b --- /dev/null +++ b/frontend/src/scripts/ape/types/ape.d.ts @@ -0,0 +1,134 @@ +declare namespace Ape { + type ClientMethod = ( + endpoint: string, + config?: RequestOptions + ) => Promise; + + 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; + payload?: any; + } + + interface Response { + status: number; + message: string; + data?: any; + } + + type EndpointData = Promise; + 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, + 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; + }; + } +} diff --git a/frontend/src/scripts/axios-instance.ts b/frontend/src/scripts/axios-instance.ts deleted file mode 100644 index a21d7bb36..000000000 --- a/frontend/src/scripts/axios-instance.ts +++ /dev/null @@ -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; diff --git a/frontend/src/scripts/controllers/account-controller.js b/frontend/src/scripts/controllers/account-controller.js index f9014ab9d..36b584fe2 100644 --- a/frontend/src/scripts/controllers/account-controller.js +++ b/frontend/src/scripts/controllers/account-controller.js @@ -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) { diff --git a/frontend/src/scripts/controllers/verification-controller.ts b/frontend/src/scripts/controllers/verification-controller.ts index ecbf02f78..4bcad3903 100644 --- a/frontend/src/scripts/controllers/verification-controller.ts +++ b/frontend/src/scripts/controllers/verification-controller.ts @@ -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 { 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(); } diff --git a/frontend/src/scripts/db.ts b/frontend/src/scripts/db.ts index eb1945f7b..cc0d01e42 100644 --- a/frontend/src/scripts/db.ts +++ b/frontend/src/scripts/db.ts @@ -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 { 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[]; + 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[]; + 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( mode: M, mode2: MonkeyTypes.Mode2, @@ -622,13 +573,13 @@ export async function saveLocalTagPB( return; } -export function updateLbMemory( +export async function updateLbMemory( mode: M, mode2: MonkeyTypes.Mode2, language: string, rank: number, api = false -): void { +): Promise { //could dbSnapshot just be used here instead of getSnapshot() if (mode === "time") { @@ -648,12 +599,7 @@ export function updateLbMemory( 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( export async function saveConfig(config: MonkeyTypes.Config): Promise { 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); } } diff --git a/frontend/src/scripts/elements/leaderboards.ts b/frontend/src/scripts/elements/leaderboards.ts index 01131ccea..edfa8fedc 100644 --- a/frontend/src/scripts/elements/leaderboards.ts +++ b/frontend/src/scripts/elements/leaderboards.ts @@ -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( - ` (${Math.abs( - difference - )} since you last checked)` - ); + if (memory !== 0) { + $(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append( + ` (${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( - ` (${Math.abs( - difference - )} since you last checked)` - ); + if (memory !== 0) { + $(`#leaderboardsWrapper table.${side} tfoot tr td .top`).append( + ` (${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 { $("#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 { @@ -350,17 +333,17 @@ async function requestMore(lb: LbKey, prepend = false): Promise { 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 { async function requestNew(lb: LbKey, skip: number): Promise { 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; } diff --git a/frontend/src/scripts/elements/notifications.ts b/frontend/src/scripts/elements/notifications.ts index 53bd31fca..1873e58c4 100644 --- a/frontend/src/scripts/elements/notifications.ts +++ b/frontend/src/scripts/elements/notifications.ts @@ -58,7 +58,7 @@ class Notification { cls = "bad"; icon = ``; title = "Error"; - console.trace(this.message); + console.error(this.message); } if (this.customTitle != undefined) { diff --git a/frontend/src/scripts/elements/psa.ts b/frontend/src/scripts/elements/psa.ts index c152c6479..e4f1526a7 100644 --- a/frontend/src/scripts/elements/psa.ts +++ b/frontend/src/scripts/elements/psa.ts @@ -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 { - 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 { diff --git a/frontend/src/scripts/popups/edit-preset-popup.ts b/frontend/src/scripts/popups/edit-preset-popup.ts index 54ed59242..7f1fb607d 100644 --- a/frontend/src/scripts/popups/edit-preset-popup.ts +++ b/frontend/src/scripts/popups/edit-preset-popup.ts @@ -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 { 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(); } }); diff --git a/frontend/src/scripts/popups/edit-tags-popup.ts b/frontend/src/scripts/popups/edit-tags-popup.ts index 5a3144f4a..7989ae373 100644 --- a/frontend/src/scripts/popups/edit-tags-popup.ts +++ b/frontend/src/scripts/popups/edit-tags-popup.ts @@ -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 { - // 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 { 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 { 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 { 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 { ResultFilters.updateTags(); } } + Loader.hide(); } $("#tagsWrapper").click((e) => { diff --git a/frontend/src/scripts/popups/quote-approve-popup.ts b/frontend/src/scripts/popups/quote-approve-popup.ts index 7069f6edf..17823ceb9 100644 --- a/frontend/src/scripts/popups/quote-approve-popup.ts +++ b/frontend/src/scripts/popups/quote-approve-popup.ts @@ -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 { 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 { @@ -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) => { diff --git a/frontend/src/scripts/popups/quote-rate-popup.ts b/frontend/src/scripts/popups/quote-rate-popup.ts index 09d2c9cca..4d0c3d41d 100644 --- a/frontend/src/scripts/popups/quote-rate-popup.ts +++ b/frontend/src/scripts/popups/quote-rate-popup.ts @@ -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 = 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 { - 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 { - 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) => { diff --git a/frontend/src/scripts/popups/quote-report-popup.ts b/frontend/src/scripts/popups/quote-report-popup.ts index b2b49c5fd..b876cc775 100644 --- a/frontend/src/scripts/popups/quote-report-popup.ts +++ b/frontend/src/scripts/popups/quote-report-popup.ts @@ -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 { async function submitReport(): Promise { 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) => { diff --git a/frontend/src/scripts/popups/quote-submit-popup.ts b/frontend/src/scripts/popups/quote-submit-popup.ts index e56ec1ec3..d6690abd9 100644 --- a/frontend/src/scripts/popups/quote-submit-popup.ts +++ b/frontend/src/scripts/popups/quote-submit-popup.ts @@ -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 { @@ -26,40 +25,29 @@ import { AxiosError } from "axios"; // } async function submitQuote(): Promise { - 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 diff --git a/frontend/src/scripts/popups/result-tags-popup.ts b/frontend/src/scripts/popups/result-tags-popup.ts index 53ab97a35..d6743401d 100644 --- a/frontend/src/scripts/popups/result-tags-popup.ts +++ b/frontend/src/scripts/popups/result-tags-popup.ts @@ -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) => { - 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) => { + 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" + ); + } }); diff --git a/frontend/src/scripts/popups/simple-popups.ts b/frontend/src/scripts/popups/simple-popups.ts index b343725bb..ab9d1bfa6 100644 --- a/frontend/src/scripts/popups/simple-popups.ts +++ b/frontend/src/scripts/popups/simple-popups.ts @@ -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(); }, () => { // diff --git a/frontend/src/scripts/test/test-logic.js b/frontend/src/scripts/test/test-logic.js index 4b5c1b476..3ff505215 100644 --- a/frontend/src/scripts/test/test-logic.js +++ b/frontend/src/scripts/test/test-logic.js @@ -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) { diff --git a/frontend/src/scripts/types/types.d.ts b/frontend/src/scripts/types/types.d.ts index 20a2fcb20..3046025fd 100644 --- a/frontend/src/scripts/types/types.d.ts +++ b/frontend/src/scripts/types/types.d.ts @@ -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 { + tags?: string[]; + } + interface DefaultConfig extends Config { wordCount: WordsModes; } diff --git a/frontend/tsconfig.json b/frontend/tsconfig.json index a472abd19..5b705cdc3 100644 --- a/frontend/tsconfig.json +++ b/frontend/tsconfig.json @@ -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 ``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`. */, diff --git a/package-lock.json b/package-lock.json index 801609ca6..5389bd755 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index f33e7d0ce..ff21f04a3 100644 --- a/package.json +++ b/package.json @@ -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": {