diff --git a/backend/api/controllers/quote.ts b/backend/api/controllers/quote.ts index 4ea2bf5ba..0f2b3eb19 100644 --- a/backend/api/controllers/quote.ts +++ b/backend/api/controllers/quote.ts @@ -1,6 +1,6 @@ import _ from "lodash"; import { v4 as uuidv4 } from "uuid"; -import UserDAO from "../../dao/user"; +import { getUser, updateQuoteRatings } from "../../dao/user"; import ReportDAO from "../../dao/report"; import NewQuotesDao from "../../dao/new-quotes"; import QuoteRatingsDAO from "../../dao/quote-ratings"; @@ -19,7 +19,7 @@ export async function getQuotes( req: MonkeyTypes.Request ): Promise { const { uid } = req.ctx.decodedToken; - let { quoteMod } = await UserDAO.getUser(uid); + let quoteMod: boolean | undefined | string = (await getUser(uid)).quoteMod; if (quoteMod === true) quoteMod = "all"; const data = await NewQuotesDao.get(quoteMod); return new MonkeyResponse("Quote submissions retrieved", data); @@ -43,7 +43,7 @@ export async function approveQuote( const { uid } = req.ctx.decodedToken; const { quoteId, editText, editSource } = req.body; - const { name } = await UserDAO.getUser(uid); + const { name } = await getUser(uid); const data = await NewQuotesDao.approve(quoteId, editText, editSource, name); Logger.logToDb("system_quote_approved", data, uid); @@ -79,7 +79,7 @@ export async function submitRating( const { uid } = req.ctx.decodedToken; const { quoteId, rating, language } = req.body; - const user = await UserDAO.getUser(uid); + const user = await getUser(uid); if (!user) { throw new MonkeyError(401, "User not found."); } @@ -107,7 +107,7 @@ export async function submitRating( Object ); - await UserDAO.updateQuoteRatings(uid, userQuoteRatings); + await updateQuoteRatings(uid, userQuoteRatings); const responseMessage = `Rating ${ shouldUpdateRating ? "updated" : "submitted" diff --git a/backend/api/controllers/result.ts b/backend/api/controllers/result.ts index a0e795b68..fca46bcf6 100644 --- a/backend/api/controllers/result.ts +++ b/backend/api/controllers/result.ts @@ -1,5 +1,11 @@ import ResultDAO from "../../dao/result"; -import UserDAO from "../../dao/user"; +import { + getUser, + checkIfPb, + checkIfTagPb, + incrementBananas, + updateTypingStats, +} from "../../dao/user"; import PublicStatsDAO from "../../dao/public-stats"; import BotDAO from "../../dao/bot"; import { roundTo2, stdDev } from "../../utils/misc"; @@ -192,7 +198,7 @@ export async function addResult( // } - const user = await UserDAO.getUser(uid); + const user = await getUser(uid); //check keyspacing and duration here for bots if ( @@ -239,8 +245,8 @@ export async function addResult( if (!result.bailedOut) { [isPb, tagPbs] = await Promise.all([ - UserDAO.checkIfPb(uid, user, result), - UserDAO.checkIfTagPb(uid, user, result), + checkIfPb(uid, user, result), + checkIfTagPb(uid, user, result), ]); } @@ -249,7 +255,7 @@ export async function addResult( } if (result.mode === "time" && String(result.mode2) === "60") { - UserDAO.incrementBananas(uid, result.wpm); + incrementBananas(uid, result.wpm); if (isPb && user.discordId) { if (useRedisForBotTasks) { George.updateDiscordRole(user.discordId, result.wpm); @@ -273,7 +279,7 @@ export async function addResult( afk = 0; } tt = result.testDuration + result.incompleteTestSeconds - afk; - UserDAO.updateTypingStats(uid, result.restartCount, tt); + updateTypingStats(uid, result.restartCount, tt); PublicStatsDAO.updateStats(result.restartCount, tt); if (result.bailedOut === false) delete result.bailedOut; diff --git a/backend/api/controllers/user.ts b/backend/api/controllers/user.ts index 1d7c535e7..bd0bc1889 100644 --- a/backend/api/controllers/user.ts +++ b/backend/api/controllers/user.ts @@ -1,4 +1,4 @@ -import UsersDAO from "../../dao/user"; +import * as UserDAL from "../../dao/user"; import BotDAO from "../../dao/bot"; import MonkeyError from "../../utils/error"; import Logger from "../../utils/logger"; @@ -13,7 +13,7 @@ export async function createNewUser( const { name } = req.body; const { email, uid } = req.ctx.decodedToken; - await UsersDAO.addUser(name, email, uid); + await UserDAL.addUser(name, email, uid); Logger.logToDb("user_created", `${name} ${email}`, uid); return new MonkeyResponse("User created"); @@ -24,8 +24,8 @@ export async function deleteUser( ): Promise { const { uid } = req.ctx.decodedToken; - const userInfo = await UsersDAO.getUser(uid); - await UsersDAO.deleteUser(uid); + const userInfo = await UserDAL.getUser(uid); + await UserDAL.deleteUser(uid); Logger.logToDb("user_deleted", `${userInfo.email} ${userInfo.name}`, uid); return new MonkeyResponse("User deleted"); @@ -37,8 +37,8 @@ export async function updateName( const { uid } = req.ctx.decodedToken; const { name } = req.body; - const oldUser = await UsersDAO.getUser(uid); - await UsersDAO.updateName(uid, name); + const oldUser = await UserDAL.getUser(uid); + await UserDAL.updateName(uid, name); Logger.logToDb( "user_name_updated", `changed name from ${oldUser.name} to ${name}`, @@ -53,7 +53,7 @@ export async function clearPb( ): Promise { const { uid } = req.ctx.decodedToken; - await UsersDAO.clearPb(uid); + await UserDAL.clearPb(uid); Logger.logToDb("user_cleared_pbs", "", uid); return new MonkeyResponse("User's PB cleared"); @@ -64,7 +64,7 @@ export async function checkName( ): Promise { const { name } = req.params; - const available = await UsersDAO.isNameAvailable(name); + const available = await UserDAL.isNameAvailable(name); if (!available) { throw new MonkeyError(409, "Username unavailable"); } @@ -79,7 +79,7 @@ export async function updateEmail( const { newEmail } = req.body; try { - await UsersDAO.updateEmail(uid, newEmail); + await UserDAL.updateEmail(uid, newEmail); } catch (e) { throw new MonkeyError(404, e.message, "update email", uid); } @@ -96,10 +96,10 @@ export async function getUser( let userInfo; try { - userInfo = await UsersDAO.getUser(uid); + userInfo = await UserDAL.getUser(uid); } catch (e) { if (email && uid) { - userInfo = await UsersDAO.addUser(undefined, email, uid); + userInfo = await UserDAL.addUser(undefined, email, uid); } else { throw new MonkeyError( 404, @@ -126,7 +126,7 @@ export async function linkDiscord( const useRedisForBotTasks = req.ctx.configuration.useRedisForBotTasks.enabled; - const userInfo = await UsersDAO.getUser(uid); + const userInfo = await UserDAL.getUser(uid); if (userInfo.discordId) { throw new MonkeyError( 409, @@ -147,7 +147,7 @@ export async function linkDiscord( ); } - const discordIdAvailable = await UsersDAO.isDiscordIdAvailable(discordId); + const discordIdAvailable = await UserDAL.isDiscordIdAvailable(discordId); if (!discordIdAvailable) { throw new MonkeyError( 409, @@ -155,7 +155,7 @@ export async function linkDiscord( ); } - await UsersDAO.linkDiscord(uid, discordId); + await UserDAL.linkDiscord(uid, discordId); if (useRedisForBotTasks) { George.linkDiscord(discordId, uid); @@ -173,7 +173,7 @@ export async function unlinkDiscord( const useRedisForBotTasks = req.ctx.configuration.useRedisForBotTasks.enabled; - const userInfo = await UsersDAO.getUser(uid); + const userInfo = await UserDAL.getUser(uid); if (!userInfo.discordId) { throw new MonkeyError(404, "User does not have a linked Discord account"); } @@ -183,7 +183,7 @@ export async function unlinkDiscord( } await BotDAO.unlinkDiscord(uid, userInfo.discordId); - await UsersDAO.unlinkDiscord(uid); + await UserDAL.unlinkDiscord(uid); Logger.logToDb("user_discord_unlinked", userInfo.discordId, uid); return new MonkeyResponse("Discord account unlinked"); @@ -195,7 +195,7 @@ export async function addTag( const { uid } = req.ctx.decodedToken; const { tagName } = req.body; - const tag = await UsersDAO.addTag(uid, tagName); + const tag = await UserDAL.addTag(uid, tagName); return new MonkeyResponse("Tag updated", tag); } @@ -205,7 +205,7 @@ export async function clearTagPb( const { uid } = req.ctx.decodedToken; const { tagId } = req.params; - await UsersDAO.removeTagPb(uid, tagId); + await UserDAL.removeTagPb(uid, tagId); return new MonkeyResponse("Tag PB cleared"); } @@ -215,7 +215,7 @@ export async function editTag( const { uid } = req.ctx.decodedToken; const { tagId, newName } = req.body; - await UsersDAO.editTag(uid, tagId, newName); + await UserDAL.editTag(uid, tagId, newName); return new MonkeyResponse("Tag updated"); } @@ -225,7 +225,7 @@ export async function removeTag( const { uid } = req.ctx.decodedToken; const { tagId } = req.params; - await UsersDAO.removeTag(uid, tagId); + await UserDAL.removeTag(uid, tagId); return new MonkeyResponse("Tag deleted"); } @@ -234,7 +234,7 @@ export async function getTags( ): Promise { const { uid } = req.ctx.decodedToken; - const tags = await UsersDAO.getTags(uid); + const tags = await UserDAL.getTags(uid); return new MonkeyResponse("Tags retrieved", tags ?? []); } @@ -242,9 +242,10 @@ export async function updateLbMemory( req: MonkeyTypes.Request ): Promise { const { uid } = req.ctx.decodedToken; - const { mode, mode2, language, rank } = req.body; + const { mode, language, rank } = req.body; + const mode2 = req.body.mode2 as MonkeyTypes.Mode2; - await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank); + await UserDAL.updateLbMemory(uid, mode, mode2, language, rank); return new MonkeyResponse("Leaderboard memory updated"); } @@ -252,7 +253,7 @@ export async function getCustomThemes( req: MonkeyTypes.Request ): Promise { const { uid } = req.ctx.decodedToken; - const customThemes = await UsersDAO.getThemes(uid); + const customThemes = await UserDAL.getThemes(uid); return new MonkeyResponse("Custom themes retrieved", customThemes); } @@ -262,7 +263,7 @@ export async function addCustomTheme( const { uid } = req.ctx.decodedToken; const { name, colors } = req.body; - const addedTheme = await UsersDAO.addTheme(uid, { name, colors }); + const addedTheme = await UserDAL.addTheme(uid, { name, colors }); return new MonkeyResponse("Custom theme added", { theme: addedTheme, }); @@ -273,7 +274,7 @@ export async function removeCustomTheme( ): Promise { const { uid } = req.ctx.decodedToken; const { themeId } = req.body; - await UsersDAO.removeTheme(uid, themeId); + await UserDAL.removeTheme(uid, themeId); return new MonkeyResponse("Custom theme removed"); } @@ -283,7 +284,7 @@ export async function editCustomTheme( const { uid } = req.ctx.decodedToken; const { themeId, theme } = req.body; - await UsersDAO.editTheme(uid, themeId, theme); + await UserDAL.editTheme(uid, themeId, theme); return new MonkeyResponse("Custom theme updated"); } @@ -293,6 +294,11 @@ export async function getPersonalBests( const { uid } = req.ctx.decodedToken; const { mode, mode2 } = req.query; - const data = (await UsersDAO.getPersonalBests(uid, mode, mode2)) ?? null; + const data = + (await UserDAL.getPersonalBests( + uid, + mode as string, + mode2 as string | undefined + )) ?? null; return new MonkeyResponse("Personal bests retrieved", data); } diff --git a/backend/dao/result.js b/backend/dao/result.js index e2e1bffeb..338359424 100644 --- a/backend/dao/result.js +++ b/backend/dao/result.js @@ -2,13 +2,13 @@ import { ObjectId } from "mongodb"; import MonkeyError from "../utils/error"; import db from "../init/db"; -import UserDAO from "./user"; +import { getUser, getTags } from "./user"; class ResultDAO { static async addResult(uid, result) { let user; try { - user = await UserDAO.getUser(uid); + user = await getUser(uid); } catch (e) { user = null; } @@ -30,7 +30,7 @@ class ResultDAO { .collection("results") .findOne({ _id: new ObjectId(resultid), uid }); if (!result) throw new MonkeyError(404, "Result not found"); - const userTags = await UserDAO.getTags(uid); + const userTags = await getTags(uid); const userTagIds = userTags.map((tag) => tag._id.toString()); let validTags = true; tags.forEach((tagId) => { diff --git a/backend/dao/user.js b/backend/dao/user.js deleted file mode 100644 index fcb5fc547..000000000 --- a/backend/dao/user.js +++ /dev/null @@ -1,404 +0,0 @@ -import _ from "lodash"; -import { isUsernameValid } from "../utils/validation"; -import { updateUserEmail } from "../utils/auth"; -import { checkAndUpdatePb } from "../utils/pb"; -import db from "../init/db"; -import MonkeyError from "../utils/error"; -import { ObjectId } from "mongodb"; - -class UsersDAO { - static async addUser(name, email, uid) { - const user = await db.collection("users").findOne({ uid }); - if (user) { - throw new MonkeyError(409, "User document already exists", "addUser"); - } - return await db - .collection("users") - .insertOne({ name, email, uid, addedAt: Date.now() }); - } - - static async deleteUser(uid) { - return await db.collection("users").deleteOne({ uid }); - } - - static async updateName(uid, name) { - if (!this.isNameAvailable(name)) { - throw new MonkeyError(409, "Username already taken", name); - } - 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 db - .collection("users") - .updateOne({ uid }, { $set: { name, lastNameChange: Date.now() } }); - } - - static async clearPb(uid) { - return await db - .collection("users") - .updateOne({ uid }, { $set: { personalBests: {}, lbPersonalBests: {} } }); - } - - static async isNameAvailable(name) { - const nameDocs = await db - .collection("users") - .find({ name }) - .collation({ locale: "en", strength: 1 }) - .limit(1) - .toArray(); - if (nameDocs.length !== 0) { - return false; - } else { - return true; - } - } - - static async updateQuoteRatings(uid, quoteRatings) { - const user = await db.collection("users").findOne({ uid }); - if (!user) { - throw new MonkeyError(404, "User not found", "updateQuoteRatings"); - } - await db.collection("users").updateOne({ uid }, { $set: { quoteRatings } }); - return true; - } - - static async updateEmail(uid, email) { - const user = await db.collection("users").findOne({ uid }); - if (!user) throw new MonkeyError(404, "User not found", "update email"); - await updateUserEmail(uid, email); - await db.collection("users").updateOne({ uid }, { $set: { email } }); - return true; - } - - static async getUser(uid) { - const user = await db.collection("users").findOne({ uid }); - if (!user) throw new MonkeyError(404, "User not found", "get user"); - return user; - } - - static async isDiscordIdAvailable(discordId) { - const user = await db.collection("users").findOne({ discordId }); - return _.isNil(user); - } - - static async addTag(uid, name) { - const _id = new ObjectId(); - await db - .collection("users") - .updateOne({ uid }, { $push: { tags: { _id, name } } }); - return { - _id, - name, - }; - } - - static async getTags(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 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 db.collection("users").updateOne( - { - uid: uid, - "tags._id": new ObjectId(_id), - }, - { $set: { "tags.$.name": name } } - ); - } - - static async removeTag(uid, _id) { - 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 db.collection("users").updateOne( - { - uid: uid, - "tags._id": new ObjectId(_id), - }, - { $pull: { tags: { _id: new ObjectId(_id) } } } - ); - } - - static async removeTagPb(uid, _id) { - 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 db.collection("users").updateOne( - { - uid: uid, - "tags._id": new ObjectId(_id), - }, - { $set: { "tags.$.personalBests": {} } } - ); - } - - static async updateLbMemory(uid, mode, mode2, language, rank) { - 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 db.collection("users").updateOne( - { uid }, - { - $set: { lbMemory: user.lbMemory }, - } - ); - } - - static async checkIfPb(uid, user, result) { - const { mode, funbox } = result; - - if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { - return false; - } - - if (mode === "quote") { - return false; - } - - let lbpb = user.lbPersonalBests; - if (!lbpb) lbpb = {}; - - let pb = checkAndUpdatePb(user.personalBests, lbpb, result); - - if (pb.isPb) { - await db - .collection("users") - .updateOne({ uid }, { $set: { personalBests: pb.obj } }); - if (pb.lbObj) { - await db - .collection("users") - .updateOne({ uid }, { $set: { lbPersonalBests: pb.lbObj } }); - } - return true; - } else { - return false; - } - } - - static async checkIfTagPb(uid, user, result) { - if (user.tags === undefined || user.tags.length === 0) { - return []; - } - - const { mode, tags, funbox } = result; - - if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { - return []; - } - - if (mode === "quote") { - return []; - } - - let tagsToCheck = []; - user.tags.forEach((tag) => { - tags.forEach((resultTag) => { - if (resultTag == tag._id) { - tagsToCheck.push(tag); - } - }); - }); - - let ret = []; - - tagsToCheck.forEach(async (tag) => { - let tagpb = checkAndUpdatePb(tag.personalBests, undefined, result); - if (tagpb.isPb) { - ret.push(tag._id); - await db - .collection("users") - .updateOne( - { uid, "tags._id": new ObjectId(tag._id) }, - { $set: { "tags.$.personalBests": tagpb.obj } } - ); - } - }); - - return ret; - } - - static async resetPb(uid) { - const user = await db.collection("users").findOne({ uid }); - if (!user) throw new MonkeyError(404, "User not found", "reset pb"); - return await db - .collection("users") - .updateOne({ uid }, { $set: { personalBests: {} } }); - } - - static async updateTypingStats(uid, restartCount, timeTyping) { - return await db.collection("users").updateOne( - { uid }, - { - $inc: { - startedTests: restartCount + 1, - completedTests: 1, - timeTyping, - }, - } - ); - } - - static async linkDiscord(uid, discordId) { - const user = await db.collection("users").findOne({ uid }); - if (!user) throw new MonkeyError(404, "User not found", "link discord"); - return await db - .collection("users") - .updateOne({ uid }, { $set: { discordId } }); - } - - static async unlinkDiscord(uid) { - const user = await db.collection("users").findOne({ uid }); - if (!user) throw new MonkeyError(404, "User not found", "unlink discord"); - return await db - .collection("users") - .updateOne({ uid }, { $set: { discordId: null } }); - } - - static async incrementBananas(uid, wpm) { - const user = await db.collection("users").findOne({ uid }); - if (!user) { - throw new MonkeyError(404, "User not found", "increment bananas"); - } - - let best60; - try { - best60 = Math.max(...user.personalBests.time[60].map((best) => best.wpm)); - } catch (e) { - best60 = undefined; - } - - if (best60 === undefined || wpm >= best60 - best60 * 0.25) { - //increment when no record found or wpm is within 25% of the record - return await db - .collection("users") - .updateOne({ uid }, { $inc: { bananas: 1 } }); - } else { - return null; - } - } - - static themeDoesNotExist(customThemes, id) { - return ( - (customThemes ?? []).filter((t) => t._id.toString() === id).length === 0 - ); - } - - static async addTheme(uid, theme) { - const user = await db.collection("users").findOne({ uid }); - if (!user) throw new MonkeyError(404, "User not found", "Add custom theme"); - - if ((user.customThemes ?? []).length >= 10) { - throw new MonkeyError(409, "Too many custom themes"); - } - - const _id = new ObjectId(); - await db.collection("users").updateOne( - { uid }, - { - $push: { - customThemes: { - _id, - name: theme.name, - colors: theme.colors, - }, - }, - } - ); - - return { - _id, - name: theme.name, - }; - } - - static async removeTheme(uid, _id) { - const user = await db.collection("users").findOne({ uid }); - if (!user) { - throw new MonkeyError(404, "User not found", "Remove custom theme"); - } - - if (this.themeDoesNotExist(user.customThemes, _id)) { - throw new MonkeyError(404, "Custom theme not found"); - } - - return await db.collection("users").updateOne( - { - uid: uid, - "customThemes._id": new ObjectId(_id), - }, - { $pull: { customThemes: { _id: new ObjectId(_id) } } } - ); - } - - static async editTheme(uid, _id, theme) { - const user = await db.collection("users").findOne({ uid }); - if (!user) { - throw new MonkeyError(404, "User not found", "Edit custom theme"); - } - - if (this.themeDoesNotExist(user.customThemes, _id)) { - throw new MonkeyError(404, "Custom Theme not found"); - } - - return await db.collection("users").updateOne( - { - uid: uid, - "customThemes._id": new ObjectId(_id), - }, - { - $set: { - "customThemes.$.name": theme.name, - "customThemes.$.colors": theme.colors, - }, - } - ); - } - - static async getThemes(uid) { - const user = await db.collection("users").findOne({ uid }); - if (!user) { - throw new MonkeyError(404, "User not found", "Get custom themes"); - } - return user.customThemes ?? []; - } - - static async getPersonalBests(uid, mode, mode2) { - const user = await db.collection("users").findOne({ uid }); - if (mode2) { - return user?.personalBests?.[mode]?.[mode2]; - } else { - return user?.personalBests?.[mode]; - } - } -} - -export default UsersDAO; diff --git a/backend/dao/user.ts b/backend/dao/user.ts new file mode 100644 index 000000000..a58373053 --- /dev/null +++ b/backend/dao/user.ts @@ -0,0 +1,536 @@ +import _ from "lodash"; +import { isUsernameValid } from "../utils/validation"; +import { updateUserEmail } from "../utils/auth"; +import { checkAndUpdatePb } from "../utils/pb"; +import db from "../init/db"; +import MonkeyError from "../utils/error"; +import { DeleteResult, InsertOneResult, ObjectId, UpdateResult } from "mongodb"; + +export async function addUser( + name: string | undefined, + email: string, + uid: string +): Promise> { + const usersCollection = db.collection("users"); + + const user = await usersCollection.findOne({ uid }); + if (user) { + throw new MonkeyError(409, "User document already exists", "addUser"); + } + + const currentDate = Date.now(); + return await usersCollection.insertOne({ + name, + email, + uid, + addedAt: currentDate, + }); +} + +export async function deleteUser(uid: string): Promise { + return await db.collection("users").deleteOne({ uid }); +} + +export async function updateName( + uid: string, + name: string +): Promise { + if (!isNameAvailable(name)) { + throw new MonkeyError(409, "Username already taken", name); + } + + const user = await db.collection("users").findOne({ uid }); + + if (!user) { + throw new MonkeyError(404, "User not found", "update name"); + } + + if (Date.now() - (user.lastNameChange ?? 0) < 2592000000) { + throw new MonkeyError(409, "You can change your name once every 30 days"); + } + if (!isUsernameValid(name)) { + throw new MonkeyError(400, "Invalid username"); + } + + return await db + .collection("users") + .updateOne({ uid }, { $set: { name, lastNameChange: Date.now() } }); +} + +export async function clearPb(uid: string): Promise { + return await db.collection("users").updateOne( + { uid }, + { + $set: { + personalBests: { + custom: {}, + quote: {}, + time: {}, + words: {}, + zen: {}, + }, + lbPersonalBests: { + time: {}, + }, + }, + } + ); +} + +export async function isNameAvailable(name: string): Promise { + const nameDocs = await db + .collection("users") + .find({ name }) + .collation({ locale: "en", strength: 1 }) + .limit(1) + .toArray(); + + return nameDocs.length === 0; +} + +export async function updateQuoteRatings( + uid: string, + quoteRatings: MonkeyTypes.UserQuoteRatings +): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) { + throw new MonkeyError(404, "User not found", "updateQuoteRatings"); + } + + await db + .collection("users") + .updateOne({ uid }, { $set: { quoteRatings } }); + return true; +} + +export async function updateEmail( + uid: string, + email: string +): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "update email"); + await updateUserEmail(uid, email); + await db + .collection("users") + .updateOne({ uid }, { $set: { email } }); + return true; +} + +export async function getUser(uid: string): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "get user"); + return user; +} + +export async function isDiscordIdAvailable( + discordId: string +): Promise { + const user = await db + .collection("users") + .findOne({ discordId }); + return _.isNil(user); +} + +export async function addTag( + uid: string, + name: string +): Promise { + const _id = new ObjectId(); + await db + .collection("users") + .updateOne({ uid }, { $push: { tags: { _id, name } } }); + return { + _id, + name, + }; +} + +export async function getTags(uid: string): Promise { + const user = await db.collection("users").findOne({ uid }); + + if (!user) throw new MonkeyError(404, "User not found", "get tags"); + + return user.tags ?? []; +} + +export async function editTag( + uid: string, + _id: string, + name: string +): Promise { + 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.toHexString() === _id).length === 0 + ) { + throw new MonkeyError(404, "Tag not found"); + } + return await db.collection("users").updateOne( + { + uid: uid, + "tags._id": new ObjectId(_id), + }, + { $set: { "tags.$.name": name } } + ); +} + +export async function removeTag( + uid: string, + _id: string +): Promise { + 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.toHexString() == _id).length === 0 + ) { + throw new MonkeyError(404, "Tag not found"); + } + return await db.collection("users").updateOne( + { + uid: uid, + "tags._id": new ObjectId(_id), + }, + { $pull: { tags: { _id: new ObjectId(_id) } } } + ); +} + +export async function removeTagPb( + uid: string, + _id: string +): Promise { + const usersCollection = db.collection("users"); + const user = await usersCollection.findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "remove tag pb"); + if ( + user.tags === undefined || + user.tags.filter((t) => t._id.toHexString() == _id).length === 0 + ) { + throw new MonkeyError(404, "Tag not found"); + } + return await usersCollection.updateOne( + { + uid: uid, + "tags._id": new ObjectId(_id), + }, + { $set: { "tags.$.personalBests": {} } } + ); +} + +export async function updateLbMemory( + uid: string, + mode: MonkeyTypes.Mode, + mode2: MonkeyTypes.Mode2, + language: string, + rank: number +): Promise { + 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 db.collection("users").updateOne( + { uid }, + { + $set: { lbMemory: user.lbMemory }, + } + ); +} + +export async function checkIfPb( + uid: string, + user: MonkeyTypes.User, + result: MonkeyTypes.Result +): Promise { + const { mode, funbox } = result; + + if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { + return false; + } + + if (mode === "quote") { + return false; + } + + let lbPb = user.lbPersonalBests; + if (!lbPb) lbPb = { time: {} }; + + const pb = checkAndUpdatePb( + user.personalBests ?? { + time: {}, + custom: {}, + quote: {}, + words: {}, + zen: {}, + }, + lbPb, + result + ); + + if (!pb.isPb) return false; + + await db + .collection("users") + .updateOne({ uid }, { $set: { personalBests: pb.personalBests } }); + + if (pb.lbPersonalBests) { + await db + .collection("users") + .updateOne({ uid }, { $set: { lbPersonalBests: pb.lbPersonalBests } }); + } + return true; +} + +export async function checkIfTagPb( + uid: string, + user: MonkeyTypes.User, + result: MonkeyTypes.Result +): Promise { + if (user.tags === undefined || user.tags.length === 0) { + return []; + } + + const { mode, tags: resultTags, funbox } = result; + + if (funbox !== "none" && funbox !== "plus_one" && funbox !== "plus_two") { + return []; + } + + if (mode === "quote") { + return []; + } + + const tagsToCheck: MonkeyTypes.UserTag[] = []; + user.tags.forEach((userTag) => { + resultTags.forEach((resultTag) => { + if (resultTag === userTag._id.toHexString()) { + tagsToCheck.push(userTag); + } + }); + }); + + const ret: string[] = []; + + tagsToCheck.forEach(async (tag) => { + const tagPbs: MonkeyTypes.PersonalBests = tag.personalBests ?? { + time: {}, + words: {}, + zen: {}, + custom: {}, + quote: {}, + }; + + const tagpb = checkAndUpdatePb(tagPbs, undefined, result); + if (tagpb.isPb) { + ret.push(tag._id.toHexString()); + await db + .collection("users") + .updateOne( + { uid, "tags._id": new ObjectId(tag._id) }, + { $set: { "tags.$.personalBests": tagpb.personalBests } } + ); + } + }); + + return ret; +} + +export async function resetPb(uid: string): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "reset pb"); + return await db.collection("users").updateOne( + { uid }, + { + $set: { + personalBests: { + time: {}, + custom: {}, + quote: {}, + words: {}, + zen: {}, + }, + }, + } + ); +} + +export async function updateTypingStats( + uid: string, + restartCount: number, + timeTyping: number +): Promise { + return await db.collection("users").updateOne( + { uid }, + { + $inc: { + startedTests: restartCount + 1, + completedTests: 1, + timeTyping, + }, + } + ); +} + +export async function linkDiscord( + uid: string, + discordId: string +): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "link discord"); + return await db + .collection("users") + .updateOne({ uid }, { $set: { discordId } }); +} + +export async function unlinkDiscord(uid: string): Promise { + const usersCollection = db.collection("users"); + const user = await usersCollection.findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "unlink discord"); + + return await usersCollection.updateOne( + { uid }, + { $set: { discordId: undefined } } + ); +} + +export async function incrementBananas( + uid: string, + wpm +): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) { + throw new MonkeyError(404, "User not found", "increment bananas"); + } + + let best60: number | undefined; + const personalBests60 = user.personalBests?.time[60]; + + if (personalBests60) { + best60 = Math.max(...personalBests60.map((best) => best.wpm)); + } + + if (best60 === undefined || wpm >= best60 - best60 * 0.25) { + //increment when no record found or wpm is within 25% of the record + return await db + .collection("users") + .updateOne({ uid }, { $inc: { bananas: 1 } }); + } + + return null; +} + +export function themeDoesNotExist(customThemes, id): boolean { + return ( + (customThemes ?? []).filter((t) => t._id.toString() === id).length === 0 + ); +} + +export async function addTheme( + uid: string, + theme +): Promise<{ _id: ObjectId; name: string }> { + const user = await db.collection("users").findOne({ uid }); + if (!user) throw new MonkeyError(404, "User not found", "Add custom theme"); + + if ((user.customThemes ?? []).length >= 10) { + throw new MonkeyError(409, "Too many custom themes"); + } + + const _id = new ObjectId(); + await db.collection("users").updateOne( + { uid }, + { + $push: { + customThemes: { + _id, + name: theme.name, + colors: theme.colors, + }, + }, + } + ); + + return { + _id, + name: theme.name, + }; +} + +export async function removeTheme(uid: string, _id): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) { + throw new MonkeyError(404, "User not found", "Remove custom theme"); + } + + if (themeDoesNotExist(user.customThemes, _id)) { + throw new MonkeyError(404, "Custom theme not found"); + } + + return await db.collection("users").updateOne( + { + uid: uid, + "customThemes._id": new ObjectId(_id), + }, + { $pull: { customThemes: { _id: new ObjectId(_id) } } } + ); +} + +export async function editTheme( + uid: string, + _id, + theme +): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) { + throw new MonkeyError(404, "User not found", "Edit custom theme"); + } + + if (themeDoesNotExist(user.customThemes, _id)) { + throw new MonkeyError(404, "Custom Theme not found"); + } + + return await db.collection("users").updateOne( + { + uid: uid, + "customThemes._id": new ObjectId(_id), + }, + { + $set: { + "customThemes.$.name": theme.name, + "customThemes.$.colors": theme.colors, + }, + } + ); +} + +export async function getThemes( + uid: string +): Promise { + const user = await db.collection("users").findOne({ uid }); + if (!user) { + throw new MonkeyError(404, "User not found", "Get custom themes"); + } + return user.customThemes ?? []; +} + +export async function getPersonalBests( + uid: string, + mode: string, + mode2?: string +): Promise { + const user = await db.collection("users").findOne({ uid }); + + if (!user) { + throw new MonkeyError(404, "User not found", "Get personal bests"); + } + + if (mode2) { + return user?.personalBests?.[mode]?.[mode2]; + } + + return user?.personalBests?.[mode]; +} diff --git a/backend/middlewares/api-utils.ts b/backend/middlewares/api-utils.ts index a5dbdae77..f2f8cd3c4 100644 --- a/backend/middlewares/api-utils.ts +++ b/backend/middlewares/api-utils.ts @@ -3,7 +3,7 @@ import joi from "joi"; import MonkeyError from "../utils/error"; import { Response, NextFunction, RequestHandler } from "express"; import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; -import UsersDAO from "../dao/user"; +import { getUser } from "../dao/user"; interface ValidationOptions { criteria: (data: T) => boolean; @@ -52,9 +52,7 @@ function checkUserPermissions( try { const { uid } = req.ctx.decodedToken; - const userData = (await UsersDAO.getUser( - uid - )) as unknown as MonkeyTypes.User; + const userData = (await getUser(uid)) as unknown as MonkeyTypes.User; const hasPermission = criteria(userData); if (!hasPermission) { diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts index fae6562ab..33fc7ca37 100644 --- a/backend/types/types.d.ts +++ b/backend/types/types.d.ts @@ -1,3 +1,5 @@ +type ObjectId = import("mongodb").ObjectId; + type ExpressRequest = import("express").Request; declare namespace MonkeyTypes { @@ -49,19 +51,21 @@ declare namespace MonkeyTypes { interface User { // TODO, Complete the typings for the user model addedAt: number; - bananas: number; - completedTests: number; + verified?: boolean; + bananas?: number; + completedTests?: number; discordId?: string; email: string; - lastNameChange: number; - lbMemory: object; - lbPersonalBests: object; - name: string; - personalBests: object; - quoteRatings?: Record>; - startedTests: number; - tags: object[]; - timeTyping: number; + lastNameChange?: number; + lbMemory?: object; + lbPersonalBests?: LbPersonalBests; + name?: string; + customThemes?: CustomTheme[]; + personalBests?: PersonalBests; + quoteRatings?: UserQuoteRatings; + startedTests?: number; + tags?: UserTag[]; + timeTyping?: number; uid: string; quoteMod?: boolean; cannotReport?: boolean; @@ -69,6 +73,28 @@ declare namespace MonkeyTypes { canManageApeKeys?: boolean; } + type UserQuoteRatings = Record>; + + interface LbPersonalBests { + time: { + [key: number]: { + [key: string]: PersonalBest; + }; + }; + } + + interface UserTag { + _id: ObjectId; + name: string; + personalBests?: PersonalBests; + } + + interface CustomTheme { + _id: ObjectId; + name: string; + colors: string[]; + } + interface ApeKey { uid: string; name: string; @@ -106,9 +132,9 @@ declare namespace MonkeyTypes { [key: number]: PersonalBest[]; }; quote: { [quote: string]: PersonalBest[] }; - custom: { custom: PersonalBest[] }; + custom: { custom?: PersonalBest[] }; zen: { - zen: PersonalBest[]; + zen?: PersonalBest[]; }; } diff --git a/backend/utils/pb.ts b/backend/utils/pb.ts index e12e4d6d5..18527bd20 100644 --- a/backend/utils/pb.ts +++ b/backend/utils/pb.ts @@ -2,18 +2,19 @@ import _ from "lodash"; interface CheckAndUpdatePbResult { isPb: boolean; - obj: object; - lbObj?: object; + personalBests: MonkeyTypes.PersonalBests; + lbPersonalBests?: MonkeyTypes.LbPersonalBests; } type Result = MonkeyTypes.Result; export function checkAndUpdatePb( - userPersonalBests: MonkeyTypes.User["personalBests"], - lbPersonalBests: MonkeyTypes.User["lbPersonalBests"] | undefined, + userPersonalBests: MonkeyTypes.PersonalBests, + lbPersonalBests: MonkeyTypes.LbPersonalBests | undefined, result: Result ): CheckAndUpdatePbResult { - const { mode, mode2 } = result; + const mode = result.mode; + const mode2 = result.mode2 as 15 | 60; const userPb = userPersonalBests ?? {}; userPb[mode] = userPb[mode] ?? {}; @@ -40,8 +41,8 @@ export function checkAndUpdatePb( return { isPb, - obj: userPb, - lbObj: lbPersonalBests, + personalBests: userPb, + lbPersonalBests: lbPersonalBests, }; } @@ -95,15 +96,16 @@ function buildPersonalBest(result: Result): MonkeyTypes.PersonalBest { } function updateLeaderboardPersonalBests( - userPersonalBests: MonkeyTypes.User["personalBests"], - lbPersonalBests: MonkeyTypes.User["lbPersonalBests"], + userPersonalBests: MonkeyTypes.PersonalBests, + lbPersonalBests: MonkeyTypes.LbPersonalBests, result: Result ): void { if (!shouldUpdateLeaderboardPersonalBests(result)) { return; } - const { mode, mode2 } = result; + const mode = result.mode; + const mode2 = result.mode2 as MonkeyTypes.Mode2<"time">; lbPersonalBests[mode] = lbPersonalBests[mode] ?? {}; const lbMode2 = lbPersonalBests[mode][mode2]; diff --git a/frontend/src/scripts/db.ts b/frontend/src/scripts/db.ts index 6b1e8c99b..c42ed2746 100644 --- a/frontend/src/scripts/db.ts +++ b/frontend/src/scripts/db.ts @@ -700,8 +700,8 @@ export async function updateLbMemory( //could dbSnapshot just be used here instead of getSnapshot() if (mode === "time") { - const timeMode = mode as "time", - timeMode2 = mode2 as 15 | 60; + const timeMode = mode as "time"; + const timeMode2 = mode2 as 15 | 60; const snapshot = getSnapshot(); if (snapshot.lbMemory === undefined) {