monkeytype/backend/dao/user.js
Jack 34e730c6fc
Custom themes storage (#2660)
* Fixed typo

* Created method for adding theme in the UserDAO:

* Created function for checking if custom theme object is valid

* Exported the isThemeValid function

* Added controller for adding customTheme

* Created route for adding custom theme

* Created rateLimit for adding customTheme

* Fixed typo

* Fixed incorrect color length

* Added method for removing, getting and editing custom themes on the backend

* Moved validations from controllers to routes and some aesthetic changes in the user dao

* Started working on frontend and some minor changes in the backend

- Commandline support for custom themes
- Allow user to shift to their custom theme using Shift-click
- Updated the backend to be compatible with some changes
- Create a new custom theme for users with old system to prevent their custom theme loss

* Fixed custom theme type in ape and now new custom theme is created if user clicks the custom tab and doesn't already have one

* Fixed ape type issue

* Format html file

* Fixed wrong tab being active

* Created new custom theme edit section

* Fixed bug where user theme would have impact on icons with custom theme

* Update customThemes API

* Updated the custom theme sharing option to work with multiple custom themes

* Started working on the UI for custom theme buttons

* Added DOM event for clicking custom theme buttons

* Updated the updateActiveButton to work with multiple custom themes

* Removed favorite button for themes and fixed bug where double theme buttons were being added

* Fixed bug where preset theme buttons were not appearing if user has applied custom theme on website load and refreshed

* Moved DOM event for sharing custom theme to more appropriate place

* Integrated the save custom theme button with the changes

* Fixed bug with custom theme tab buttons and theme buttons

* Fixed commented div

* Replaced 'sds' with a meaningful message for custom theme buttons

* Integrated the delete button for deleting custom themes and fixed bug where id of newly added custom theme was not set properly

* Integrated the add button and name field for custom themes editing

* Added addCustomThemeWrapper element

I added it before but seems like vscode and other editors can't handle large files

* Removed some debug statements

* Removed some more debug statements

* Used parial types for custom theme. Thanks Bruce

* Removed unnecssary try catch blocks. Thanks Bruce

* Rephrased custom theme API messages

* Set new theme fields explicitly to prevent validtion failures and rephrased API message

* Replaced let with const

* Replaced let with const for _id

* Replaced let with const and used nullish coalescing

* Improved code quality in User DAO

* Strict equality in user DAO

* Moved validation scheme to a variables at the top of file

* Fixed bugs with strict equality checks

* Renamed themeId to themeID for consistency

* Made customThemes a required type in db to remove unnecessary undefined checks

* Uncommented GET API endpoint

* Prevent colorId being updated on custom theme name chnage

* Removed debug log

* Added loader on api calls

* Commenced shift from customThemeIndex to customThemeId

* Added required to themeColors schema

* Temp fix for validation fail for customThemeId

* Changed default value of customThemeId back to ''

* Temp fix for validation fail for customThemeId

* Fixed minor bug

* Fixed bug where account-controller would pass undefined to ThemeController.set

* Created methods in db.ts for adding, deleting and editing custom themes. Created new interface for raw custom themes and renamed ape methods

* Removed repeating code in account-controller

* Removed repeating code in theme-picker

* Removed setThemes in config

* Fixed minor bug

* Removed repeating code in user DAO

* Made custom themes available to registered users only

* Fixed minor bug

* Removed debug log and updated custom theme commands before showing list

* Added popup for confirming custom theme deletion

* Added custom option for random theme

* Minor improvement

* Workaround for local config firing before firebase initalization

* Removed debug log and created workaround for migration

* Added legacy customTheme config field

* Replaced workaround

* Changed put to patch

* Changed put to patch

* Added customTheme field back

* Integrated customTheme into to feature

* Added notifications for users when they access custom theme cmd option without being logged in

* Removed debug logs and comments

* Replaced literals with constant. Thanks Bruception

* Fixed wrong querySelector parameters and reset custom theme colors after deleting a custom theme

* added notification on save

* duplicating object instead of referencing

* Added return type on function

* Fixed wrong notification code

* spreading default config instead of referencing

* added index, psas, configs, presets

* camel_case

* added ape keys, leaderboards, results, quotes

* Modified setCustomTheme

* Modified setCustomThemeId

* Added tip for random themes settings

* Modified setCustomThemeId

* Now load custom theme before account loading

* Added custom theme compatibility for non-logged in users to theme-controller

* Now update tabs and buttons on custom theme config value change and modified boolean checks to use customTheme instead of customThemeId

* Fixed bug

* Refactoring in theme-controller.ts

* Enable custom theme support in commandline for logged out users

* More refactoring in theme-controller.ts

* Added custom theme compatibility for logged out users

* Removed double events in settings.ts and now turn on custom theme upon applying

* Fixed bug and recursive call

* Fixed bug

* Fixed random theme custom option

* Fix jquery wrong syntax

* Readded notification upon custom theme edit

* One notification upon error only

* Change notification type

* New custom themes now have default colors

* Notification on custom theme edit for non-logged in users

* Refresh buttons upon settings load

* missing gitignore

* updated message

* updated message

* setting config to unchanged when logging in to avoid issues with applying db config

* reverted some over complicated code, excessive auth checks

* removed customthemeid from config

* not setting custom theme id

* removed all customthemeid references

* removed commented code

* removed name field

* added edit button

* unused file

* removed popup

* removed add button, removed text

* removed duplicate code

* added simple popup checkbox support

* whitespace

* added custom theme popups

* removed warning when no custom themes were found

* removed add button click handler

* added function to save custom theme

* saving current theme not default

* removed custom theme id from default config

* not creating new theme by default, just applying

* reacting to customThemeColors save

* unnecessary function call

* removed unused code

* small refactor

* spacing

* unnecessary code

* turned off warnings for non null asertion

* showing theme name when randomising customs

* Revert "turned off warnings for non null asertion"

This reverts commit 433e1dc767.

* optional with default instead

* fixed custom theme colors always loaded on page load

* fixed custom theme buttons not showing up

* fixed various loading issues

* fixed custom theme edit styles

* showing custom in footer, removed unused code

* savaing custom theme colors
fixed typos

* changing theme

* updated custom theme buttons styling

* scaling custom theme buttons on hover

* not updating settings on theme event

* fixed quote id

* only showing custom themes when logged in

* updating save button text depending on auth state

* fixed double notification when trying to save too many custom themes

* fixed custom theme saving when signed out

* removed user check from db

* fixed exception when signed out user tried to open the custom themes command line

* ignoring file when compiling

* typo

* avoiding href errors

* setting href to an existing file
this fixes firefox custom themes not working

* better hex color regex

* spacing

* renamed function

* typo

* destructuring request

* removed unused function

* removed unused code

* removed unused code

* type fix

* removed non capturing group

* saving colors to config before saving custom theme

* encoding in base64

* added handler that can load themes in the old and new format from the url

Co-authored-by: Rizwan Mustafa <rizwanmustafa0000@gmail.com>
Co-authored-by: Rizwan Mustafa <69350358+rizwanmustafa@users.noreply.github.com>
2022-03-09 19:48:22 +01:00

390 lines
11 KiB
JavaScript

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;