From beb7e35dcb94fec86e3a8961fba2b5bc7cf71f36 Mon Sep 17 00:00:00 2001 From: Bruce Berrios <58147810+Bruception@users.noreply.github.com> Date: Wed, 30 Mar 2022 18:20:13 -0400 Subject: [PATCH] Remove class syntax from controllers (#2791) * Remove class syntax from controllers * Specify base and rename --- backend/api/controllers/ape-key.ts | 82 ++++ backend/api/controllers/ape-keys.ts | 83 ---- backend/api/controllers/config.ts | 32 +- backend/api/controllers/leaderboard.ts | 54 +++ backend/api/controllers/leaderboards.ts | 54 --- backend/api/controllers/preset.ts | 72 ++-- backend/api/controllers/psa.ts | 12 +- backend/api/controllers/quote.ts | 138 ++++++ backend/api/controllers/quotes.ts | 128 ------ backend/api/controllers/result.ts | 510 +++++++++++----------- backend/api/controllers/user.ts | 540 ++++++++++++------------ backend/api/routes/ape-keys.ts | 10 +- backend/api/routes/configs.ts | 2 +- backend/api/routes/leaderboards.ts | 6 +- backend/api/routes/presets.ts | 2 +- backend/api/routes/psas.ts | 4 +- backend/api/routes/quotes.ts | 16 +- backend/api/routes/results.ts | 2 +- backend/api/routes/users.ts | 2 +- 19 files changed, 889 insertions(+), 860 deletions(-) create mode 100644 backend/api/controllers/ape-key.ts delete mode 100644 backend/api/controllers/ape-keys.ts create mode 100644 backend/api/controllers/leaderboard.ts delete mode 100644 backend/api/controllers/leaderboards.ts create mode 100644 backend/api/controllers/quote.ts delete mode 100644 backend/api/controllers/quotes.ts diff --git a/backend/api/controllers/ape-key.ts b/backend/api/controllers/ape-key.ts new file mode 100644 index 000000000..2c2cd72c1 --- /dev/null +++ b/backend/api/controllers/ape-key.ts @@ -0,0 +1,82 @@ +import _ from "lodash"; +import { randomBytes } from "crypto"; +import { hash } from "bcrypt"; +import ApeKeysDAO from "../../dao/ape-keys"; +import MonkeyError from "../../utils/error"; +import { MonkeyResponse } from "../../utils/monkey-response"; +import { base64UrlEncode } from "../../utils/misc"; + +function cleanApeKey(apeKey: MonkeyTypes.ApeKey): Partial { + return _.omit(apeKey, "hash", "_id", "uid", "useCount"); +} + +export async function getApeKeys( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + const apeKeys = await ApeKeysDAO.getApeKeys(uid); + const cleanedKeys = _(apeKeys).keyBy("_id").mapValues(cleanApeKey).value(); + + return new MonkeyResponse("ApeKeys retrieved", cleanedKeys); +} + +export async function generateApeKey( + req: MonkeyTypes.Request +): Promise { + const { name, enabled } = req.body; + const { uid } = req.ctx.decodedToken; + const { maxKeysPerUser, apeKeyBytes, apeKeySaltRounds } = + req.ctx.configuration.apeKeys; + + const currentNumberOfApeKeys = await ApeKeysDAO.countApeKeysForUser(uid); + + if (currentNumberOfApeKeys >= maxKeysPerUser) { + throw new MonkeyError(409, "Maximum number of ApeKeys have been generated"); + } + + const apiKey = randomBytes(apeKeyBytes).toString("base64url"); + const saltyHash = await hash(apiKey, apeKeySaltRounds); + + const apeKey: MonkeyTypes.ApeKey = { + name, + enabled, + uid, + hash: saltyHash, + createdOn: Date.now(), + modifiedOn: Date.now(), + lastUsedOn: -1, + useCount: 0, + }; + + const apeKeyId = await ApeKeysDAO.addApeKey(apeKey); + + return new MonkeyResponse("ApeKey generated", { + apeKey: base64UrlEncode(`${apeKeyId}.${apiKey}`), + apeKeyId, + apeKeyDetails: cleanApeKey(apeKey), + }); +} + +export async function editApeKey( + req: MonkeyTypes.Request +): Promise { + const { apeKeyId } = req.params; + const { name, enabled } = req.body; + const { uid } = req.ctx.decodedToken; + + await ApeKeysDAO.editApeKey(uid, apeKeyId, name, enabled); + + return new MonkeyResponse("ApeKey updated"); +} + +export async function deleteApeKey( + req: MonkeyTypes.Request +): Promise { + const { apeKeyId } = req.params; + const { uid } = req.ctx.decodedToken; + + await ApeKeysDAO.deleteApeKey(uid, apeKeyId); + + return new MonkeyResponse("ApeKey deleted"); +} diff --git a/backend/api/controllers/ape-keys.ts b/backend/api/controllers/ape-keys.ts deleted file mode 100644 index 40b881abb..000000000 --- a/backend/api/controllers/ape-keys.ts +++ /dev/null @@ -1,83 +0,0 @@ -import _ from "lodash"; -import { randomBytes } from "crypto"; -import { hash } from "bcrypt"; -import ApeKeysDAO from "../../dao/ape-keys"; -import MonkeyError from "../../utils/error"; -import { MonkeyResponse } from "../../utils/monkey-response"; -import { base64UrlEncode } from "../../utils/misc"; - -function cleanApeKey(apeKey: MonkeyTypes.ApeKey): Partial { - return _.omit(apeKey, "hash", "_id", "uid", "useCount"); -} - -class ApeKeysController { - static async getApeKeys(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - - const apeKeys = await ApeKeysDAO.getApeKeys(uid); - const hashlessKeys = _(apeKeys).keyBy("_id").mapValues(cleanApeKey).value(); - - return new MonkeyResponse("ApeKeys retrieved", hashlessKeys); - } - - static async generateApeKey( - req: MonkeyTypes.Request - ): Promise { - const { name, enabled } = req.body; - const { uid } = req.ctx.decodedToken; - const { maxKeysPerUser, apeKeyBytes, apeKeySaltRounds } = - req.ctx.configuration.apeKeys; - - const currentNumberOfApeKeys = await ApeKeysDAO.countApeKeysForUser(uid); - - if (currentNumberOfApeKeys >= maxKeysPerUser) { - throw new MonkeyError( - 409, - "Maximum number of ApeKeys have been generated" - ); - } - - const apiKey = randomBytes(apeKeyBytes).toString("base64url"); - const saltyHash = await hash(apiKey, apeKeySaltRounds); - - const apeKey: MonkeyTypes.ApeKey = { - name, - enabled, - uid, - hash: saltyHash, - createdOn: Date.now(), - modifiedOn: Date.now(), - lastUsedOn: -1, - useCount: 0, - }; - - const apeKeyId = await ApeKeysDAO.addApeKey(apeKey); - - return new MonkeyResponse("ApeKey generated", { - apeKey: base64UrlEncode(`${apeKeyId}.${apiKey}`), - apeKeyId, - apeKeyDetails: cleanApeKey(apeKey), - }); - } - - static async editApeKey(req: MonkeyTypes.Request): Promise { - const { apeKeyId } = req.params; - const { name, enabled } = req.body; - const { uid } = req.ctx.decodedToken; - - await ApeKeysDAO.editApeKey(uid, apeKeyId, name, enabled); - - return new MonkeyResponse("ApeKey updated"); - } - - static async deleteApeKey(req: MonkeyTypes.Request): Promise { - const { apeKeyId } = req.params; - const { uid } = req.ctx.decodedToken; - - await ApeKeysDAO.deleteApeKey(uid, apeKeyId); - - return new MonkeyResponse("ApeKey deleted"); - } -} - -export default ApeKeysController; diff --git a/backend/api/controllers/config.ts b/backend/api/controllers/config.ts index b32ffd9af..4225a4c87 100644 --- a/backend/api/controllers/config.ts +++ b/backend/api/controllers/config.ts @@ -1,22 +1,22 @@ import ConfigDAO from "../../dao/config"; import { MonkeyResponse } from "../../utils/monkey-response"; -class ConfigController { - static async getConfig(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; +export async function getConfig( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; - const data = await ConfigDAO.getConfig(uid); - return new MonkeyResponse("Configuration retrieved", data); - } - - static async saveConfig(req: MonkeyTypes.Request): Promise { - const { config } = req.body; - const { uid } = req.ctx.decodedToken; - - await ConfigDAO.saveConfig(uid, config); - - return new MonkeyResponse("Config updated"); - } + const data = await ConfigDAO.getConfig(uid); + return new MonkeyResponse("Configuration retrieved", data); } -export default ConfigController; +export async function saveConfig( + req: MonkeyTypes.Request +): Promise { + const { config } = req.body; + const { uid } = req.ctx.decodedToken; + + await ConfigDAO.saveConfig(uid, config); + + return new MonkeyResponse("Config updated"); +} diff --git a/backend/api/controllers/leaderboard.ts b/backend/api/controllers/leaderboard.ts new file mode 100644 index 000000000..ac431d9cb --- /dev/null +++ b/backend/api/controllers/leaderboard.ts @@ -0,0 +1,54 @@ +import _ from "lodash"; +import { MonkeyResponse } from "../../utils/monkey-response"; +import LeaderboardsDAO from "../../dao/leaderboards"; + +export async function getLeaderboard( + req: MonkeyTypes.Request +): Promise { + const { language, mode, mode2, skip, limit = 50 } = req.query; + const { uid } = req.ctx.decodedToken; + + const queryLimit = Math.min(parseInt(limit as string, 10), 50); + + const leaderboard = await LeaderboardsDAO.get( + mode, + mode2, + language, + parseInt(skip as string, 10), + queryLimit + ); + + if (leaderboard === false) { + return new MonkeyResponse( + "Leaderboard is currently updating. Please try again in a few seconds.", + null, + 503 + ); + } + + const normalizedLeaderboard = _.map(leaderboard as any[], (entry) => { + return uid && entry.uid === uid + ? entry + : _.omit(entry, ["discordId", "uid", "difficulty", "language"]); + }); + + return new MonkeyResponse("Leaderboard retrieved", normalizedLeaderboard); +} + +export async function getRankFromLeaderboard( + req: MonkeyTypes.Request +): Promise { + const { language, mode, mode2 } = req.query; + const { uid } = req.ctx.decodedToken; + + const data = await LeaderboardsDAO.getRank(mode, mode2, language, uid); + if (data === false) { + return new MonkeyResponse( + "Leaderboard is currently updating. Please try again in a few seconds.", + null, + 503 + ); + } + + return new MonkeyResponse("Rank retrieved", data); +} diff --git a/backend/api/controllers/leaderboards.ts b/backend/api/controllers/leaderboards.ts deleted file mode 100644 index f9b325d61..000000000 --- a/backend/api/controllers/leaderboards.ts +++ /dev/null @@ -1,54 +0,0 @@ -import _ from "lodash"; -import { MonkeyResponse } from "../../utils/monkey-response"; -import LeaderboardsDAO from "../../dao/leaderboards"; - -class LeaderboardsController { - static async get(req: MonkeyTypes.Request): Promise { - const { language, mode, mode2, skip, limit = 50 } = req.query; - const { uid } = req.ctx.decodedToken; - - const queryLimit = Math.min(parseInt(limit as string, 10), 50); - - const leaderboard = await LeaderboardsDAO.get( - mode, - mode2, - language, - parseInt(skip as string, 10), - queryLimit - ); - - if (leaderboard === false) { - return new MonkeyResponse( - "Leaderboard is currently updating. Please try again in a few seconds.", - null, - 503 - ); - } - - const normalizedLeaderboard = _.map(leaderboard as any[], (entry) => { - return uid && entry.uid === uid - ? entry - : _.omit(entry, ["discordId", "uid", "difficulty", "language"]); - }); - - return new MonkeyResponse("Leaderboard retrieved", normalizedLeaderboard); - } - - static async getRank(req: MonkeyTypes.Request): Promise { - const { language, mode, mode2 } = req.query; - const { uid } = req.ctx.decodedToken; - - const data = await LeaderboardsDAO.getRank(mode, mode2, language, uid); - if (data === false) { - return new MonkeyResponse( - "Leaderboard is currently updating. Please try again in a few seconds.", - null, - 503 - ); - } - - return new MonkeyResponse("Rank retrieved", data); - } -} - -export default LeaderboardsController; diff --git a/backend/api/controllers/preset.ts b/backend/api/controllers/preset.ts index f7164065f..c21e206bf 100644 --- a/backend/api/controllers/preset.ts +++ b/backend/api/controllers/preset.ts @@ -1,40 +1,44 @@ import PresetDAO from "../../dao/preset"; import { MonkeyResponse } from "../../utils/monkey-response"; -class PresetController { - static async getPresets(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; +export async function getPresets( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; - const data = await PresetDAO.getPresets(uid); - return new MonkeyResponse("Preset retrieved", data); - } - - static async addPreset(req: MonkeyTypes.Request): Promise { - const { name, config } = req.body; - const { uid } = req.ctx.decodedToken; - - const data = await PresetDAO.addPreset(uid, name, config); - - return new MonkeyResponse("Preset created", data); - } - - static async editPreset(req: MonkeyTypes.Request): Promise { - const { _id, name, config } = req.body; - const { uid } = req.ctx.decodedToken; - - await PresetDAO.editPreset(uid, _id, name, config); - - return new MonkeyResponse("Preset updated"); - } - - static async removePreset(req: MonkeyTypes.Request): Promise { - const { presetId } = req.params; - const { uid } = req.ctx.decodedToken; - - await PresetDAO.removePreset(uid, presetId); - - return new MonkeyResponse("Preset deleted"); - } + const data = await PresetDAO.getPresets(uid); + return new MonkeyResponse("Preset retrieved", data); } -export default PresetController; +export async function addPreset( + req: MonkeyTypes.Request +): Promise { + const { name, config } = req.body; + const { uid } = req.ctx.decodedToken; + + const data = await PresetDAO.addPreset(uid, name, config); + + return new MonkeyResponse("Preset created", data); +} + +export async function editPreset( + req: MonkeyTypes.Request +): Promise { + const { _id, name, config } = req.body; + const { uid } = req.ctx.decodedToken; + + await PresetDAO.editPreset(uid, _id, name, config); + + return new MonkeyResponse("Preset updated"); +} + +export async function removePreset( + req: MonkeyTypes.Request +): Promise { + const { presetId } = req.params; + const { uid } = req.ctx.decodedToken; + + await PresetDAO.removePreset(uid, presetId); + + return new MonkeyResponse("Preset deleted"); +} diff --git a/backend/api/controllers/psa.ts b/backend/api/controllers/psa.ts index 027c44a70..bae81126e 100644 --- a/backend/api/controllers/psa.ts +++ b/backend/api/controllers/psa.ts @@ -1,11 +1,9 @@ import PsaDAO from "../../dao/psa"; import { MonkeyResponse } from "../../utils/monkey-response"; -class PsaController { - static async get(_req: MonkeyTypes.Request): Promise { - const data = await PsaDAO.get(); - return new MonkeyResponse("PSAs retrieved", data); - } +export async function getPsas( + _req: MonkeyTypes.Request +): Promise { + const data = await PsaDAO.get(); + return new MonkeyResponse("PSAs retrieved", data); } - -export default PsaController; diff --git a/backend/api/controllers/quote.ts b/backend/api/controllers/quote.ts new file mode 100644 index 000000000..c41dcdb98 --- /dev/null +++ b/backend/api/controllers/quote.ts @@ -0,0 +1,138 @@ +import _ from "lodash"; +import { v4 as uuidv4 } from "uuid"; +import UserDAO from "../../dao/user"; +import ReportDAO from "../../dao/report"; +import NewQuotesDao from "../../dao/new-quotes"; +import QuoteRatingsDAO from "../../dao/quote-ratings"; +import MonkeyError from "../../utils/error"; +import { verify } from "../../utils/captcha"; +import Logger from "../../utils/logger"; +import { MonkeyResponse } from "../../utils/monkey-response"; + +async function verifyCaptcha(captcha: string): Promise { + if (!(await verify(captcha))) { + throw new MonkeyError(422, "Captcha check failed"); + } +} + +export async function getQuotes( + _req: MonkeyTypes.Request +): Promise { + const data = await NewQuotesDao.get(); + return new MonkeyResponse("Quote submissions retrieved", data); +} + +export async function addQuote( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { text, source, language, captcha } = req.body; + + await verifyCaptcha(captcha); + + await NewQuotesDao.add(text, source, language, uid); + return new MonkeyResponse("Quote submission added"); +} + +export async function approveQuote( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { quoteId, editText, editSource } = req.body; + + const data = await NewQuotesDao.approve(quoteId, editText, editSource); + Logger.logToDb("system_quote_approved", data, uid); + + return new MonkeyResponse(data.message, data.quote); +} + +export async function refuseQuote( + req: MonkeyTypes.Request +): Promise { + const { quoteId } = req.body; + + await NewQuotesDao.refuse(quoteId); + return new MonkeyResponse("Quote refused"); +} + +export async function getRating( + req: MonkeyTypes.Request +): Promise { + const { quoteId, language } = req.query; + + const data = await QuoteRatingsDAO.get( + parseInt(quoteId as string, 10), + language as string + ); + + return new MonkeyResponse("Rating retrieved", data); +} + +export async function submitRating( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { quoteId, rating, language } = req.body; + + const user = await UserDAO.getUser(uid); + if (!user) { + throw new MonkeyError(401, "User not found."); + } + + const normalizedQuoteId = parseInt(quoteId as string, 10); + const normalizedRating = Math.round(parseInt(rating as string, 10)); + + const userQuoteRatings = user.quoteRatings ?? {}; + const currentRating = userQuoteRatings[language]?.[normalizedQuoteId] ?? 0; + + const newRating = normalizedRating - currentRating; + const shouldUpdateRating = currentRating !== 0; + + await QuoteRatingsDAO.submit( + quoteId, + language, + newRating, + shouldUpdateRating + ); + + _.setWith( + userQuoteRatings, + `[${language}][${normalizedQuoteId}]`, + normalizedRating, + Object + ); + + await UserDAO.updateQuoteRatings(uid, userQuoteRatings); + + const responseMessage = `Rating ${ + shouldUpdateRating ? "updated" : "submitted" + }`; + return new MonkeyResponse(responseMessage); +} + +export async function reportQuote( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { + quoteReport: { maxReports, contentReportLimit }, + } = req.ctx.configuration; + + const { quoteId, quoteLanguage, reason, comment, captcha } = req.body; + + await verifyCaptcha(captcha); + + const newReport: MonkeyTypes.Report = { + id: uuidv4(), + type: "quote", + timestamp: new Date().getTime(), + uid, + contentId: `${quoteLanguage}-${quoteId}`, + reason, + comment, + }; + + await ReportDAO.createReport(newReport, maxReports, contentReportLimit); + + return new MonkeyResponse("Quote reported"); +} diff --git a/backend/api/controllers/quotes.ts b/backend/api/controllers/quotes.ts deleted file mode 100644 index 32a34db3f..000000000 --- a/backend/api/controllers/quotes.ts +++ /dev/null @@ -1,128 +0,0 @@ -import _ from "lodash"; -import { v4 as uuidv4 } from "uuid"; -import UserDAO from "../../dao/user"; -import ReportDAO from "../../dao/report"; -import NewQuotesDao from "../../dao/new-quotes"; -import QuoteRatingsDAO from "../../dao/quote-ratings"; -import MonkeyError from "../../utils/error"; -import { verify } from "../../utils/captcha"; -import Logger from "../../utils/logger"; -import { MonkeyResponse } from "../../utils/monkey-response"; - -async function verifyCaptcha(captcha: string): Promise { - if (!(await verify(captcha))) { - throw new MonkeyError(422, "Captcha check failed"); - } -} - -class QuotesController { - static async getQuotes(_req: MonkeyTypes.Request): Promise { - const data = await NewQuotesDao.get(); - return new MonkeyResponse("Quote submissions retrieved", data); - } - - static async addQuote(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { text, source, language, captcha } = req.body; - - await verifyCaptcha(captcha); - - await NewQuotesDao.add(text, source, language, uid); - return new MonkeyResponse("Quote submission added"); - } - - static async approveQuote(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { quoteId, editText, editSource } = req.body; - - const data = await NewQuotesDao.approve(quoteId, editText, editSource); - Logger.logToDb("system_quote_approved", data, uid); - - return new MonkeyResponse(data.message, data.quote); - } - - static async refuseQuote(req: MonkeyTypes.Request): Promise { - const { quoteId } = req.body; - - await NewQuotesDao.refuse(quoteId); - return new MonkeyResponse("Quote refused"); - } - - static async getRating(req: MonkeyTypes.Request): Promise { - const { quoteId, language } = req.query; - - const data = await QuoteRatingsDAO.get( - parseInt(quoteId as string), - language as string - ); - - return new MonkeyResponse("Rating retrieved", data); - } - - static async submitRating(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { quoteId, rating, language } = req.body; - - const user = await UserDAO.getUser(uid); - if (!user) { - throw new MonkeyError(401, "User not found."); - } - - const normalizedQuoteId = parseInt(quoteId as string); - const normalizedRating = Math.round(parseInt(rating as string)); - - const userQuoteRatings = user.quoteRatings ?? {}; - const currentRating = userQuoteRatings[language]?.[normalizedQuoteId] ?? 0; - - const newRating = normalizedRating - currentRating; - const shouldUpdateRating = currentRating !== 0; - - await QuoteRatingsDAO.submit( - quoteId, - language, - newRating, - shouldUpdateRating - ); - - _.setWith( - userQuoteRatings, - `[${language}][${normalizedQuoteId}]`, - normalizedRating, - Object - ); - - await UserDAO.updateQuoteRatings(uid, userQuoteRatings); - - const responseMessage = `Rating ${ - shouldUpdateRating ? "updated" : "submitted" - }`; - return new MonkeyResponse(responseMessage); - } - - static async reportQuote(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { - quoteReport: { maxReports, contentReportLimit }, - } = req.ctx.configuration; - - const { quoteId, quoteLanguage, reason, comment, captcha } = req.body; - - await verifyCaptcha(captcha); - - const newReport: MonkeyTypes.Report = { - id: uuidv4(), - type: "quote", - timestamp: new Date().getTime(), - uid, - contentId: `${quoteLanguage}-${quoteId}`, - reason, - comment, - }; - - await ReportDAO.createReport(newReport, maxReports, contentReportLimit); - - return new MonkeyResponse("Quote reported"); - } -} - -export default QuotesController; diff --git a/backend/api/controllers/result.ts b/backend/api/controllers/result.ts index c527d699e..a0e795b68 100644 --- a/backend/api/controllers/result.ts +++ b/backend/api/controllers/result.ts @@ -34,74 +34,181 @@ try { } } -class ResultController { - static async getResults(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const results = await ResultDAO.getResults(uid); - return new MonkeyResponse("Result retrieved", results); +export async function getResults( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const results = await ResultDAO.getResults(uid); + return new MonkeyResponse("Result retrieved", results); +} + +export async function deleteAll( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + await ResultDAO.deleteAll(uid); + Logger.logToDb("user_results_deleted", "", uid); + return new MonkeyResponse("All results deleted"); +} + +export async function updateTags( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { tagIds, resultId } = req.body; + + await ResultDAO.updateTags(uid, resultId, tagIds); + return new MonkeyResponse("Result tags updated"); +} + +export async function addResult( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + const useRedisForBotTasks = req.ctx.configuration.useRedisForBotTasks.enabled; + + const result = Object.assign({}, req.body.result); + result.uid = uid; + if (result.wpm === result.raw && result.acc !== 100) { + const status = MonkeyStatusCodes.RESULT_DATA_INVALID; + throw new MonkeyError(status.code, "Bad input"); // todo move this + } + if (isTestTooShort(result)) { + const status = MonkeyStatusCodes.TEST_TOO_SHORT; + throw new MonkeyError(status.code, status.message); } - static async deleteAll(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - - await ResultDAO.deleteAll(uid); - Logger.logToDb("user_results_deleted", "", uid); - return new MonkeyResponse("All results deleted"); + const resulthash = result.hash; + delete result.hash; + delete result.stringified; + if ( + req.ctx.configuration.resultObjectHashCheck.enabled && + resulthash.length === 40 + ) { + //if its not 64 that means client is still using old hashing package + const serverhash = objectHash(result); + if (serverhash !== resulthash) { + Logger.logToDb( + "incorrect_result_hash", + { + serverhash, + resulthash, + result, + }, + uid + ); + const status = MonkeyStatusCodes.RESULT_HASH_INVALID; + throw new MonkeyError(status.code, "Incorrect result hash"); + } } - static async updateTags(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { tagIds, resultId } = req.body; - - await ResultDAO.updateTags(uid, resultId, tagIds); - return new MonkeyResponse("Result tags updated"); - } - - static async addResult(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - - const useRedisForBotTasks = - req.ctx.configuration.useRedisForBotTasks.enabled; - - const result = Object.assign({}, req.body.result); - result.uid = uid; - if (result.wpm === result.raw && result.acc !== 100) { + if (anticheatImplemented()) { + if (!validateResult(result)) { const status = MonkeyStatusCodes.RESULT_DATA_INVALID; - throw new MonkeyError(status.code, "Bad input"); // todo move this + throw new MonkeyError(status.code, "Result data doesn't make sense"); } - if (isTestTooShort(result)) { - const status = MonkeyStatusCodes.TEST_TOO_SHORT; - throw new MonkeyError(status.code, status.message); + } else { + if (process.env.MODE !== "dev") { + throw new Error("No anticheat module found"); } + Logger.warning( + "No anticheat module found. Continuing in dev mode, results will not be validated." + ); + } - const resulthash = result.hash; - delete result.hash; - delete result.stringified; - if ( - req.ctx.configuration.resultObjectHashCheck.enabled && - resulthash.length === 40 - ) { - //if its not 64 that means client is still using old hashing package - const serverhash = objectHash(result); - if (serverhash !== resulthash) { - Logger.logToDb( - "incorrect_result_hash", - { - serverhash, - resulthash, - result, - }, - uid - ); - const status = MonkeyStatusCodes.RESULT_HASH_INVALID; - throw new MonkeyError(status.code, "Incorrect result hash"); - } - } + //dont use - result timestamp is unreliable, can be changed by system time and stuff + // if (result.timestamp > Math.round(Date.now() / 1000) * 1000 + 10) { + // log( + // "time_traveler", + // { + // resultTimestamp: result.timestamp, + // serverTimestamp: Math.round(Date.now() / 1000) * 1000 + 10, + // }, + // uid + // ); + // return res.status(400).json({ message: "Time traveler detected" }); + // this probably wont work if we replace the timestamp with the server time later + // let timestampres = await ResultDAO.getResultByTimestamp( + // uid, + // result.timestamp + // ); + // if (timestampres) { + // return res.status(400).json({ message: "Duplicate result" }); + // } + + //convert result test duration to miliseconds + const testDurationMilis = result.testDuration * 1000; + //get latest result ordered by timestamp + let lastResultTimestamp; + try { + lastResultTimestamp = (await ResultDAO.getLastResult(uid)).timestamp; + } catch (e) { + lastResultTimestamp = null; + } + + result.timestamp = Math.floor(Date.now() / 1000) * 1000; + + //check if now is earlier than last result plus duration (-1 second as a buffer) + const earliestPossible = lastResultTimestamp + testDurationMilis; + const nowNoMilis = Math.floor(Date.now() / 1000) * 1000; + if (lastResultTimestamp && nowNoMilis < earliestPossible - 1000) { + Logger.logToDb( + "invalid_result_spacing", + { + lastTimestamp: lastResultTimestamp, + earliestPossible, + now: nowNoMilis, + testDuration: testDurationMilis, + difference: nowNoMilis - earliestPossible, + }, + uid + ); + const status = MonkeyStatusCodes.RESULT_SPACING_INVALID; + throw new MonkeyError(status.code, "Invalid result spacing"); + } + + try { + result.keySpacingStats = { + average: + result.keySpacing.reduce((previous, current) => (current += previous)) / + result.keySpacing.length, + sd: stdDev(result.keySpacing), + }; + } catch (e) { + // + } + try { + result.keyDurationStats = { + average: + result.keyDuration.reduce( + (previous, current) => (current += previous) + ) / result.keyDuration.length, + sd: stdDev(result.keyDuration), + }; + } catch (e) { + // + } + + const user = await UserDAO.getUser(uid); + + //check keyspacing and duration here for bots + if ( + result.mode === "time" && + result.wpm > 130 && + result.testDuration < 122 && + (user.verified === false || user.verified === undefined) + ) { + if (!result.keySpacingStats || !result.keyDurationStats) { + const status = MonkeyStatusCodes.MISSING_KEY_DATA; + throw new MonkeyError(status.code, "Missing key data"); + } if (anticheatImplemented()) { - if (!validateResult(result)) { - const status = MonkeyStatusCodes.RESULT_DATA_INVALID; - throw new MonkeyError(status.code, "Result data doesn't make sense"); + if (!validateKeys(result, uid)) { + const status = MonkeyStatusCodes.BOT_DETECTED; + throw new MonkeyError(status.code, "Possible bot detected"); } } else { if (process.env.MODE !== "dev") { @@ -111,202 +218,95 @@ class ResultController { "No anticheat module found. Continuing in dev mode, results will not be validated." ); } - - //dont use - result timestamp is unreliable, can be changed by system time and stuff - // if (result.timestamp > Math.round(Date.now() / 1000) * 1000 + 10) { - // log( - // "time_traveler", - // { - // resultTimestamp: result.timestamp, - // serverTimestamp: Math.round(Date.now() / 1000) * 1000 + 10, - // }, - // uid - // ); - // return res.status(400).json({ message: "Time traveler detected" }); - - // this probably wont work if we replace the timestamp with the server time later - // let timestampres = await ResultDAO.getResultByTimestamp( - // uid, - // result.timestamp - // ); - // if (timestampres) { - // return res.status(400).json({ message: "Duplicate result" }); - // } - - //convert result test duration to miliseconds - const testDurationMilis = result.testDuration * 1000; - //get latest result ordered by timestamp - let lastResultTimestamp; - try { - lastResultTimestamp = (await ResultDAO.getLastResult(uid)).timestamp; - } catch (e) { - lastResultTimestamp = null; - } - - result.timestamp = Math.floor(Date.now() / 1000) * 1000; - - //check if now is earlier than last result plus duration (-1 second as a buffer) - const earliestPossible = lastResultTimestamp + testDurationMilis; - const nowNoMilis = Math.floor(Date.now() / 1000) * 1000; - if (lastResultTimestamp && nowNoMilis < earliestPossible - 1000) { - Logger.logToDb( - "invalid_result_spacing", - { - lastTimestamp: lastResultTimestamp, - earliestPossible, - now: nowNoMilis, - testDuration: testDurationMilis, - difference: nowNoMilis - earliestPossible, - }, - uid - ); - const status = MonkeyStatusCodes.RESULT_SPACING_INVALID; - throw new MonkeyError(status.code, "Invalid result spacing"); - } - - try { - result.keySpacingStats = { - average: - result.keySpacing.reduce( - (previous, current) => (current += previous) - ) / result.keySpacing.length, - sd: stdDev(result.keySpacing), - }; - } catch (e) { - // - } - try { - result.keyDurationStats = { - average: - result.keyDuration.reduce( - (previous, current) => (current += previous) - ) / result.keyDuration.length, - sd: stdDev(result.keyDuration), - }; - } catch (e) { - // - } - - const user = await UserDAO.getUser(uid); - - //check keyspacing and duration here for bots - if ( - result.mode === "time" && - result.wpm > 130 && - result.testDuration < 122 && - (user.verified === false || user.verified === undefined) - ) { - if (!result.keySpacingStats || !result.keyDurationStats) { - const status = MonkeyStatusCodes.MISSING_KEY_DATA; - throw new MonkeyError(status.code, "Missing key data"); - } - if (anticheatImplemented()) { - if (!validateKeys(result, uid)) { - const status = MonkeyStatusCodes.BOT_DETECTED; - throw new MonkeyError(status.code, "Possible bot detected"); - } - } else { - if (process.env.MODE !== "dev") { - throw new Error("No anticheat module found"); - } - Logger.warning( - "No anticheat module found. Continuing in dev mode, results will not be validated." - ); - } - } - - delete result.keySpacing; - delete result.keyDuration; - delete result.smoothConsistency; - delete result.wpmConsistency; - - try { - result.keyDurationStats.average = roundTo2( - result.keyDurationStats.average - ); - result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd); - result.keySpacingStats.average = roundTo2(result.keySpacingStats.average); - result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd); - } catch (e) { - // - } - - let isPb = false; - let tagPbs: any[] = []; - - if (!result.bailedOut) { - [isPb, tagPbs] = await Promise.all([ - UserDAO.checkIfPb(uid, user, result), - UserDAO.checkIfTagPb(uid, user, result), - ]); - } - - if (isPb) { - result.isPb = true; - } - - if (result.mode === "time" && String(result.mode2) === "60") { - UserDAO.incrementBananas(uid, result.wpm); - if (isPb && user.discordId) { - if (useRedisForBotTasks) { - George.updateDiscordRole(user.discordId, result.wpm); - } - BotDAO.updateDiscordRole(user.discordId, result.wpm); - } - } - - if (result.challenge && user.discordId) { - if (useRedisForBotTasks) { - George.awardChallenge(user.discordId, result.challenge); - } - BotDAO.awardChallenge(user.discordId, result.challenge); - } else { - delete result.challenge; - } - - let tt = 0; - let afk = result.afkDuration; - if (afk == undefined) { - afk = 0; - } - tt = result.testDuration + result.incompleteTestSeconds - afk; - UserDAO.updateTypingStats(uid, result.restartCount, tt); - PublicStatsDAO.updateStats(result.restartCount, tt); - - if (result.bailedOut === false) delete result.bailedOut; - if (result.blindMode === false) delete result.blindMode; - if (result.lazyMode === false) delete result.lazyMode; - if (result.difficulty === "normal") delete result.difficulty; - if (result.funbox === "none") delete result.funbox; - if (result.language === "english") delete result.language; - if (result.numbers === false) delete result.numbers; - if (result.punctuation === false) delete result.punctuation; - - if (result.mode !== "custom") delete result.customText; - - const addedResult = await ResultDAO.addResult(uid, result); - - if (isPb) { - Logger.logToDb( - "user_new_pb", - `${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${ - result.rawWpm - } ${result.consistency}% (${addedResult.insertedId})`, - uid - ); - } - - const data = { - isPb, - name: result.name, - tagPbs, - insertedId: addedResult.insertedId, - }; - - incrementResult(result); - - return new MonkeyResponse("Result saved", data); } -} -export default ResultController; + delete result.keySpacing; + delete result.keyDuration; + delete result.smoothConsistency; + delete result.wpmConsistency; + + try { + result.keyDurationStats.average = roundTo2(result.keyDurationStats.average); + result.keyDurationStats.sd = roundTo2(result.keyDurationStats.sd); + result.keySpacingStats.average = roundTo2(result.keySpacingStats.average); + result.keySpacingStats.sd = roundTo2(result.keySpacingStats.sd); + } catch (e) { + // + } + + let isPb = false; + let tagPbs: any[] = []; + + if (!result.bailedOut) { + [isPb, tagPbs] = await Promise.all([ + UserDAO.checkIfPb(uid, user, result), + UserDAO.checkIfTagPb(uid, user, result), + ]); + } + + if (isPb) { + result.isPb = true; + } + + if (result.mode === "time" && String(result.mode2) === "60") { + UserDAO.incrementBananas(uid, result.wpm); + if (isPb && user.discordId) { + if (useRedisForBotTasks) { + George.updateDiscordRole(user.discordId, result.wpm); + } + BotDAO.updateDiscordRole(user.discordId, result.wpm); + } + } + + if (result.challenge && user.discordId) { + if (useRedisForBotTasks) { + George.awardChallenge(user.discordId, result.challenge); + } + BotDAO.awardChallenge(user.discordId, result.challenge); + } else { + delete result.challenge; + } + + let tt = 0; + let afk = result.afkDuration; + if (afk == undefined) { + afk = 0; + } + tt = result.testDuration + result.incompleteTestSeconds - afk; + UserDAO.updateTypingStats(uid, result.restartCount, tt); + PublicStatsDAO.updateStats(result.restartCount, tt); + + if (result.bailedOut === false) delete result.bailedOut; + if (result.blindMode === false) delete result.blindMode; + if (result.lazyMode === false) delete result.lazyMode; + if (result.difficulty === "normal") delete result.difficulty; + if (result.funbox === "none") delete result.funbox; + if (result.language === "english") delete result.language; + if (result.numbers === false) delete result.numbers; + if (result.punctuation === false) delete result.punctuation; + + if (result.mode !== "custom") delete result.customText; + + const addedResult = await ResultDAO.addResult(uid, result); + + if (isPb) { + Logger.logToDb( + "user_new_pb", + `${result.mode + " " + result.mode2} ${result.wpm} ${result.acc}% ${ + result.rawWpm + } ${result.consistency}% (${addedResult.insertedId})`, + uid + ); + } + + const data = { + isPb, + name: result.name, + tagPbs, + insertedId: addedResult.insertedId, + }; + + incrementResult(result); + + return new MonkeyResponse("Result saved", data); +} diff --git a/backend/api/controllers/user.ts b/backend/api/controllers/user.ts index f8df7529e..4103efb00 100644 --- a/backend/api/controllers/user.ts +++ b/backend/api/controllers/user.ts @@ -7,268 +7,286 @@ import { linkAccount } from "../../utils/discord"; import { buildAgentLog } from "../../utils/misc"; import George from "../../tasks/george"; -class UserController { - static async createNewUser( - req: MonkeyTypes.Request - ): Promise { - const { name } = req.body; - const { email, uid } = req.ctx.decodedToken; +export async function createNewUser( + req: MonkeyTypes.Request +): Promise { + const { name } = req.body; + const { email, uid } = req.ctx.decodedToken; - await UsersDAO.addUser(name, email, uid); - Logger.logToDb("user_created", `${name} ${email}`, uid); + await UsersDAO.addUser(name, email, uid); + Logger.logToDb("user_created", `${name} ${email}`, uid); - return new MonkeyResponse("User created"); - } - - static async deleteUser(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - - const userInfo = await UsersDAO.getUser(uid); - await UsersDAO.deleteUser(uid); - Logger.logToDb("user_deleted", `${userInfo.email} ${userInfo.name}`, uid); - - return new MonkeyResponse("User deleted"); - } - - static async updateName(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { name } = req.body; - - const oldUser = await UsersDAO.getUser(uid); - await UsersDAO.updateName(uid, name); - Logger.logToDb( - "user_name_updated", - `changed name from ${oldUser.name} to ${name}`, - uid - ); - - return new MonkeyResponse("User's name updated"); - } - - static async clearPb(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - - await UsersDAO.clearPb(uid); - Logger.logToDb("user_cleared_pbs", "", uid); - - return new MonkeyResponse("User's PB cleared"); - } - - static async checkName(req: MonkeyTypes.Request): Promise { - const { name } = req.params; - - const available = await UsersDAO.isNameAvailable(name); - if (!available) { - throw new MonkeyError(409, "Username unavailable"); - } - - return new MonkeyResponse("Username available"); - } - - static async updateEmail(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { newEmail } = req.body; - - try { - await UsersDAO.updateEmail(uid, newEmail); - } catch (e) { - throw new MonkeyError(404, e.message, "update email", uid); - } - - Logger.logToDb("user_email_updated", `changed email to ${newEmail}`, uid); - - return new MonkeyResponse("Email updated"); - } - - static async getUser(req: MonkeyTypes.Request): Promise { - const { email, uid } = req.ctx.decodedToken; - - let userInfo; - try { - userInfo = await UsersDAO.getUser(uid); - } catch (e) { - if (email && uid) { - userInfo = await UsersDAO.addUser(undefined, email, uid); - } else { - throw new MonkeyError( - 404, - "User not found. Could not recreate user document.", - "Tried to recreate user document but either email or uid is nullish", - uid - ); - } - } - - const agentLog = buildAgentLog(req); - Logger.logToDb("user_data_requested", agentLog, uid); - - return new MonkeyResponse("User data retrieved", userInfo); - } - - static async linkDiscord(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { - data: { tokenType, accessToken }, - } = req.body; - - const useRedisForBotTasks = - req.ctx.configuration.useRedisForBotTasks.enabled; - - const userInfo = await UsersDAO.getUser(uid); - if (userInfo.banned) { - throw new MonkeyError(403, "Banned accounts cannot link with Discord"); - } - - const { id: discordId } = await linkAccount(tokenType, accessToken); - - if (!discordId) { - throw new MonkeyError( - 500, - "Could not get Discord account info", - "discord id is undefined" - ); - } - - const discordIdAvailable = await UsersDAO.isDiscordIdAvailable(discordId); - if (!discordIdAvailable) { - throw new MonkeyError( - 409, - "This Discord account is already linked to a different account" - ); - } - - await UsersDAO.linkDiscord(uid, discordId); - - if (useRedisForBotTasks) { - George.linkDiscord(discordId, uid); - } - await BotDAO.linkDiscord(uid, discordId); - Logger.logToDb("user_discord_link", `linked to ${discordId}`, uid); - - return new MonkeyResponse("Discord account linked", discordId); - } - - static async unlinkDiscord( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - - const useRedisForBotTasks = - req.ctx.configuration.useRedisForBotTasks.enabled; - - const userInfo = await UsersDAO.getUser(uid); - if (!userInfo.discordId) { - throw new MonkeyError(404, "User does not have a linked Discord account"); - } - - if (useRedisForBotTasks) { - George.unlinkDiscord(userInfo.discordId, uid); - } - await BotDAO.unlinkDiscord(uid, userInfo.discordId); - - await UsersDAO.unlinkDiscord(uid); - Logger.logToDb("user_discord_unlinked", userInfo.discordId, uid); - - return new MonkeyResponse("Discord account unlinked"); - } - - static async addTag(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { tagName } = req.body; - - const tag = await UsersDAO.addTag(uid, tagName); - return new MonkeyResponse("Tag updated", tag); - } - - static async clearTagPb(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { tagId } = req.params; - - await UsersDAO.removeTagPb(uid, tagId); - return new MonkeyResponse("Tag PB cleared"); - } - - static async editTag(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { tagId, newName } = req.body; - - await UsersDAO.editTag(uid, tagId, newName); - return new MonkeyResponse("Tag updated"); - } - - static async removeTag(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - const { tagId } = req.params; - - await UsersDAO.removeTag(uid, tagId); - return new MonkeyResponse("Tag deleted"); - } - - static async getTags(req: MonkeyTypes.Request): Promise { - const { uid } = req.ctx.decodedToken; - - const tags = await UsersDAO.getTags(uid); - return new MonkeyResponse("Tags retrieved", tags ?? []); - } - - static async updateLbMemory( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - const { mode, mode2, language, rank } = req.body; - - await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank); - return new MonkeyResponse("Leaderboard memory updated"); - } - - static async getCustomThemes( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - const customThemes = await UsersDAO.getThemes(uid); - return new MonkeyResponse("Custom themes retrieved", customThemes); - } - - static async addCustomTheme( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - const { name, colors } = req.body; - - const addedTheme = await UsersDAO.addTheme(uid, { name, colors }); - return new MonkeyResponse("Custom theme added", { - theme: addedTheme, - }); - } - - static async removeCustomTheme( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - const { themeId } = req.body; - await UsersDAO.removeTheme(uid, themeId); - return new MonkeyResponse("Custom theme removed"); - } - - static async editCustomTheme( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - const { themeId, theme } = req.body; - - await UsersDAO.editTheme(uid, themeId, theme); - return new MonkeyResponse("Custom theme updated"); - } - - static async getPersonalBests( - req: MonkeyTypes.Request - ): Promise { - const { uid } = req.ctx.decodedToken; - const { mode, mode2 } = req.query; - - const data = (await UsersDAO.getPersonalBests(uid, mode, mode2)) ?? null; - return new MonkeyResponse("Personal bests retrieved", data); - } + return new MonkeyResponse("User created"); } -export default UserController; +export async function deleteUser( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + const userInfo = await UsersDAO.getUser(uid); + await UsersDAO.deleteUser(uid); + Logger.logToDb("user_deleted", `${userInfo.email} ${userInfo.name}`, uid); + + return new MonkeyResponse("User deleted"); +} + +export async function updateName( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { name } = req.body; + + const oldUser = await UsersDAO.getUser(uid); + await UsersDAO.updateName(uid, name); + Logger.logToDb( + "user_name_updated", + `changed name from ${oldUser.name} to ${name}`, + uid + ); + + return new MonkeyResponse("User's name updated"); +} + +export async function clearPb( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + await UsersDAO.clearPb(uid); + Logger.logToDb("user_cleared_pbs", "", uid); + + return new MonkeyResponse("User's PB cleared"); +} + +export async function checkName( + req: MonkeyTypes.Request +): Promise { + const { name } = req.params; + + const available = await UsersDAO.isNameAvailable(name); + if (!available) { + throw new MonkeyError(409, "Username unavailable"); + } + + return new MonkeyResponse("Username available"); +} + +export async function updateEmail( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { newEmail } = req.body; + + try { + await UsersDAO.updateEmail(uid, newEmail); + } catch (e) { + throw new MonkeyError(404, e.message, "update email", uid); + } + + Logger.logToDb("user_email_updated", `changed email to ${newEmail}`, uid); + + return new MonkeyResponse("Email updated"); +} + +export async function getUser( + req: MonkeyTypes.Request +): Promise { + const { email, uid } = req.ctx.decodedToken; + + let userInfo; + try { + userInfo = await UsersDAO.getUser(uid); + } catch (e) { + if (email && uid) { + userInfo = await UsersDAO.addUser(undefined, email, uid); + } else { + throw new MonkeyError( + 404, + "User not found. Could not recreate user document.", + "Tried to recreate user document but either email or uid is nullish", + uid + ); + } + } + + const agentLog = buildAgentLog(req); + Logger.logToDb("user_data_requested", agentLog, uid); + + return new MonkeyResponse("User data retrieved", userInfo); +} + +export async function linkDiscord( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { + data: { tokenType, accessToken }, + } = req.body; + + const useRedisForBotTasks = req.ctx.configuration.useRedisForBotTasks.enabled; + + const userInfo = await UsersDAO.getUser(uid); + if (userInfo.banned) { + throw new MonkeyError(403, "Banned accounts cannot link with Discord"); + } + + const { id: discordId } = await linkAccount(tokenType, accessToken); + + if (!discordId) { + throw new MonkeyError( + 500, + "Could not get Discord account info", + "discord id is undefined" + ); + } + + const discordIdAvailable = await UsersDAO.isDiscordIdAvailable(discordId); + if (!discordIdAvailable) { + throw new MonkeyError( + 409, + "This Discord account is already linked to a different account" + ); + } + + await UsersDAO.linkDiscord(uid, discordId); + + if (useRedisForBotTasks) { + George.linkDiscord(discordId, uid); + } + await BotDAO.linkDiscord(uid, discordId); + Logger.logToDb("user_discord_link", `linked to ${discordId}`, uid); + + return new MonkeyResponse("Discord account linked", discordId); +} + +export async function unlinkDiscord( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + const useRedisForBotTasks = req.ctx.configuration.useRedisForBotTasks.enabled; + + const userInfo = await UsersDAO.getUser(uid); + if (!userInfo.discordId) { + throw new MonkeyError(404, "User does not have a linked Discord account"); + } + + if (useRedisForBotTasks) { + George.unlinkDiscord(userInfo.discordId, uid); + } + await BotDAO.unlinkDiscord(uid, userInfo.discordId); + + await UsersDAO.unlinkDiscord(uid); + Logger.logToDb("user_discord_unlinked", userInfo.discordId, uid); + + return new MonkeyResponse("Discord account unlinked"); +} + +export async function addTag( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { tagName } = req.body; + + const tag = await UsersDAO.addTag(uid, tagName); + return new MonkeyResponse("Tag updated", tag); +} + +export async function clearTagPb( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { tagId } = req.params; + + await UsersDAO.removeTagPb(uid, tagId); + return new MonkeyResponse("Tag PB cleared"); +} + +export async function editTag( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { tagId, newName } = req.body; + + await UsersDAO.editTag(uid, tagId, newName); + return new MonkeyResponse("Tag updated"); +} + +export async function removeTag( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { tagId } = req.params; + + await UsersDAO.removeTag(uid, tagId); + return new MonkeyResponse("Tag deleted"); +} + +export async function getTags( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + + const tags = await UsersDAO.getTags(uid); + return new MonkeyResponse("Tags retrieved", tags ?? []); +} + +export async function updateLbMemory( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { mode, mode2, language, rank } = req.body; + + await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank); + return new MonkeyResponse("Leaderboard memory updated"); +} + +export async function getCustomThemes( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const customThemes = await UsersDAO.getThemes(uid); + return new MonkeyResponse("Custom themes retrieved", customThemes); +} + +export async function addCustomTheme( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { name, colors } = req.body; + + const addedTheme = await UsersDAO.addTheme(uid, { name, colors }); + return new MonkeyResponse("Custom theme added", { + theme: addedTheme, + }); +} + +export async function removeCustomTheme( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { themeId } = req.body; + await UsersDAO.removeTheme(uid, themeId); + return new MonkeyResponse("Custom theme removed"); +} + +export async function editCustomTheme( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { themeId, theme } = req.body; + + await UsersDAO.editTheme(uid, themeId, theme); + return new MonkeyResponse("Custom theme updated"); +} + +export async function getPersonalBests( + req: MonkeyTypes.Request +): Promise { + const { uid } = req.ctx.decodedToken; + const { mode, mode2 } = req.query; + + const data = (await UsersDAO.getPersonalBests(uid, mode, mode2)) ?? null; + return new MonkeyResponse("Personal bests retrieved", data); +} diff --git a/backend/api/routes/ape-keys.ts b/backend/api/routes/ape-keys.ts index da00c66a6..0f27a9619 100644 --- a/backend/api/routes/ape-keys.ts +++ b/backend/api/routes/ape-keys.ts @@ -7,7 +7,7 @@ import { validateRequest, } from "../../middlewares/api-utils"; import { authenticateRequest } from "../../middlewares/auth"; -import ApeKeysController from "../controllers/ape-keys"; +import * as ApeKeyController from "../controllers/ape-key"; import * as RateLimit from "../../middlewares/rate-limit"; const apeKeyNameSchema = joi @@ -43,7 +43,7 @@ router.get( RateLimit.apeKeysGet, authenticateRequest(), checkIfUserCanManageApeKeys, - asyncHandler(ApeKeysController.getApeKeys) + asyncHandler(ApeKeyController.getApeKeys) ); router.post( @@ -57,7 +57,7 @@ router.post( enabled: joi.boolean().required(), }, }), - asyncHandler(ApeKeysController.generateApeKey) + asyncHandler(ApeKeyController.generateApeKey) ); router.patch( @@ -74,7 +74,7 @@ router.patch( enabled: joi.boolean(), }, }), - asyncHandler(ApeKeysController.editApeKey) + asyncHandler(ApeKeyController.editApeKey) ); router.delete( @@ -87,7 +87,7 @@ router.delete( apeKeyId: joi.string().required(), }, }), - asyncHandler(ApeKeysController.deleteApeKey) + asyncHandler(ApeKeyController.deleteApeKey) ); export default router; diff --git a/backend/api/routes/configs.ts b/backend/api/routes/configs.ts index 736ba2c5e..14214ba15 100644 --- a/backend/api/routes/configs.ts +++ b/backend/api/routes/configs.ts @@ -2,7 +2,7 @@ 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 ConfigController from "../controllers/config"; import * as RateLimit from "../../middlewares/rate-limit"; const router = Router(); diff --git a/backend/api/routes/leaderboards.ts b/backend/api/routes/leaderboards.ts index 470e7eb17..6c0a8956e 100644 --- a/backend/api/routes/leaderboards.ts +++ b/backend/api/routes/leaderboards.ts @@ -3,7 +3,7 @@ import { Router } from "express"; import * as RateLimit from "../../middlewares/rate-limit"; import apeRateLimit from "../../middlewares/ape-rate-limit"; import { authenticateRequest } from "../../middlewares/auth"; -import LeaderboardsController from "../controllers/leaderboards"; +import * as LeaderboardController from "../controllers/leaderboard"; import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; const router = Router(); @@ -21,7 +21,7 @@ router.get( limit: joi.number().min(0).max(50), }, }), - asyncHandler(LeaderboardsController.get) + asyncHandler(LeaderboardController.getLeaderboard) ); router.get( @@ -36,7 +36,7 @@ router.get( mode2: joi.string().required(), }, }), - asyncHandler(LeaderboardsController.getRank) + asyncHandler(LeaderboardController.getRankFromLeaderboard) ); export default router; diff --git a/backend/api/routes/presets.ts b/backend/api/routes/presets.ts index 508867c12..5b64a9d5d 100644 --- a/backend/api/routes/presets.ts +++ b/backend/api/routes/presets.ts @@ -1,6 +1,6 @@ import joi from "joi"; import { authenticateRequest } from "../../middlewares/auth"; -import PresetController from "../controllers/preset"; +import * as PresetController from "../controllers/preset"; import * as RateLimit from "../../middlewares/rate-limit"; import configSchema from "../schemas/config-schema"; import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; diff --git a/backend/api/routes/psas.ts b/backend/api/routes/psas.ts index c00f7062e..307124e65 100644 --- a/backend/api/routes/psas.ts +++ b/backend/api/routes/psas.ts @@ -1,10 +1,10 @@ -import PsaController from "../controllers/psa"; +import * as 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)); +router.get("/", RateLimit.psaGet, asyncHandler(PsaController.getPsas)); export default router; diff --git a/backend/api/routes/quotes.ts b/backend/api/routes/quotes.ts index d8684ef0b..c6262ae9d 100644 --- a/backend/api/routes/quotes.ts +++ b/backend/api/routes/quotes.ts @@ -1,7 +1,7 @@ import joi from "joi"; import { authenticateRequest } from "../../middlewares/auth"; import { Router } from "express"; -import QuotesController from "../controllers/quotes"; +import * as QuoteController from "../controllers/quote"; import * as RateLimit from "../../middlewares/rate-limit"; import { asyncHandler, @@ -23,7 +23,7 @@ quotesRouter.get( RateLimit.newQuotesGet, authenticateRequest(), checkIfUserIsQuoteMod, - asyncHandler(QuotesController.getQuotes) + asyncHandler(QuoteController.getQuotes) ); quotesRouter.post( @@ -46,7 +46,7 @@ quotesRouter.post( }, validationErrorMessage: "Please fill all the fields", }), - asyncHandler(QuotesController.addQuote) + asyncHandler(QuoteController.addQuote) ); quotesRouter.post( @@ -62,7 +62,7 @@ quotesRouter.post( validationErrorMessage: "Please fill all the fields", }), checkIfUserIsQuoteMod, - asyncHandler(QuotesController.approveQuote) + asyncHandler(QuoteController.approveQuote) ); quotesRouter.post( @@ -75,7 +75,7 @@ quotesRouter.post( }, }), checkIfUserIsQuoteMod, - asyncHandler(QuotesController.refuseQuote) + asyncHandler(QuoteController.refuseQuote) ); quotesRouter.get( @@ -88,7 +88,7 @@ quotesRouter.get( language: joi.string().required(), }, }), - asyncHandler(QuotesController.getRating) + asyncHandler(QuoteController.getRating) ); quotesRouter.post( @@ -102,7 +102,7 @@ quotesRouter.post( language: joi.string().required(), }, }), - asyncHandler(QuotesController.submitRating) + asyncHandler(QuoteController.submitRating) ); quotesRouter.post( @@ -137,7 +137,7 @@ quotesRouter.post( return !user.cannotReport; }, }), - asyncHandler(QuotesController.reportQuote) + asyncHandler(QuoteController.reportQuote) ); export default quotesRouter; diff --git a/backend/api/routes/results.ts b/backend/api/routes/results.ts index 0344089bc..f08922c02 100644 --- a/backend/api/routes/results.ts +++ b/backend/api/routes/results.ts @@ -1,4 +1,4 @@ -import ResultController from "../controllers/result"; +import * as ResultController from "../controllers/result"; import resultSchema from "../schemas/result-schema"; import { asyncHandler, diff --git a/backend/api/routes/users.ts b/backend/api/routes/users.ts index fe35ef0fb..b83280563 100644 --- a/backend/api/routes/users.ts +++ b/backend/api/routes/users.ts @@ -1,7 +1,7 @@ import joi from "joi"; import { authenticateRequest } from "../../middlewares/auth"; import { Router } from "express"; -import UserController from "../controllers/user"; +import * as UserController from "../controllers/user"; import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; import * as RateLimit from "../../middlewares/rate-limit"; import apeRateLimit from "../../middlewares/ape-rate-limit";