diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts index 826481de3..93fe12f6e 100644 --- a/backend/src/api/controllers/user.ts +++ b/backend/src/api/controllers/user.ts @@ -25,7 +25,16 @@ export async function createNewUser( const { name, captcha } = req.body; const { email, uid } = req.ctx.decodedToken; - await verifyCaptcha(captcha); + try { + await verifyCaptcha(captcha); + } catch (e) { + try { + await admin.auth().deleteUser(uid); + } catch (e) { + // user might be deleted on the frontend + } + throw e; + } if (email.endsWith("@tidal.lol") || email.endsWith("@selfbot.cc")) { throw new MonkeyError(400, "Invalid domain"); diff --git a/backend/src/api/schemas/config-schema.ts b/backend/src/api/schemas/config-schema.ts index fd8462794..f37d1d972 100644 --- a/backend/src/api/schemas/config-schema.ts +++ b/backend/src/api/schemas/config-schema.ts @@ -81,7 +81,9 @@ const CONFIG_SCHEMA = joi.object({ soundVolume: joi.string().valid("0.1", "0.5", "1.0"), startGraphsAtZero: joi.boolean(), showOutOfFocusWarning: joi.boolean(), - paceCaret: joi.string().valid("off", "average", "pb", "last", "custom"), + paceCaret: joi + .string() + .valid("off", "average", "pb", "last", "daily", "custom"), paceCaretCustomSpeed: joi.number().min(0), repeatedPace: joi.boolean(), pageWidth: joi.string().valid("100", "125", "150", "200", "max"), diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index f1b696946..b1da61912 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -144,7 +144,7 @@ async function authenticateWithBearerToken( options: RequestAuthenticationOptions ): Promise { try { - const decodedToken = await verifyIdToken(token); + const decodedToken = await verifyIdToken(token, options.requireFreshToken); if (options.requireFreshToken) { const now = Date.now(); diff --git a/backend/src/utils/auth.ts b/backend/src/utils/auth.ts index 6294c0660..a6222f706 100644 --- a/backend/src/utils/auth.ts +++ b/backend/src/utils/auth.ts @@ -17,7 +17,14 @@ const tokenCache = new LRUCache({ const TOKEN_CACHE_BUFFER = 1000 * 60 * 5; // 5 minutes -export async function verifyIdToken(idToken: string): Promise { +export async function verifyIdToken( + idToken: string, + noCache = false +): Promise { + if (noCache) { + return await admin.auth().verifyIdToken(idToken, true); + } + setTokenCacheLength(tokenCache.size); setTokenCacheSize(tokenCache.calculatedSize ?? 0); diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 8616541c3..2040d2fe1 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -9,8 +9,8 @@ "version": "1.14.3", "license": "GPL-3.0", "dependencies": { - "@types/throttle-debounce": "2.1.0", "axios": "0.21.4", + "canvas-confetti": "1.5.1", "chart.js": "3.7.1", "chartjs-adapter-date-fns": "2.0.0", "chartjs-plugin-annotation": "1.4.0", @@ -29,6 +29,7 @@ "throttle-debounce": "3.0.1" }, "devDependencies": { + "@types/canvas-confetti": "1.4.3", "@types/chartjs-plugin-trendline": "1.0.1", "@types/damerau-levenshtein": "1.0.0", "@types/grecaptcha": "3.0.4", @@ -36,6 +37,7 @@ "@types/jquery": "3.5.14", "@types/object-hash": "2.2.1", "@types/select2": "4.0.55", + "@types/throttle-debounce": "2.1.0", "@types/tinycolor2": "1.4.3", "buffer": "6.0.3", "circular-dependency-plugin": "5.2.2", @@ -951,6 +953,12 @@ "@types/node": "*" } }, + "node_modules/@types/canvas-confetti": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.4.3.tgz", + "integrity": "sha512-UwFPTsW1ZwVyo/ETp4hPSikSD7yl2V42E3VWBF5P/0+DHO4iajyceWv7hfNdZ2AX5tkZnuViiBWOqyCPohU2FQ==", + "dev": true + }, "node_modules/@types/chartjs-plugin-trendline": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz", @@ -1158,7 +1166,8 @@ "node_modules/@types/throttle-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", - "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==" + "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==", + "dev": true }, "node_modules/@types/tinycolor2": { "version": "1.4.3", @@ -2548,6 +2557,15 @@ "url": "https://opencollective.com/browserslist" } }, + "node_modules/canvas-confetti": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.5.1.tgz", + "integrity": "sha512-Ncz+oZJP6OvY7ti4E1slxVlyAV/3g7H7oQtcCDXgwGgARxPnwYY9PW5Oe+I8uvspYNtuHviAdgA0LfcKFWJfpg==", + "funding": { + "type": "donate", + "url": "https://www.paypal.me/kirilvatev" + } + }, "node_modules/centra": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/centra/-/centra-1.0.1.tgz", @@ -15157,6 +15175,12 @@ "@types/node": "*" } }, + "@types/canvas-confetti": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/@types/canvas-confetti/-/canvas-confetti-1.4.3.tgz", + "integrity": "sha512-UwFPTsW1ZwVyo/ETp4hPSikSD7yl2V42E3VWBF5P/0+DHO4iajyceWv7hfNdZ2AX5tkZnuViiBWOqyCPohU2FQ==", + "dev": true + }, "@types/chartjs-plugin-trendline": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/chartjs-plugin-trendline/-/chartjs-plugin-trendline-1.0.1.tgz", @@ -15364,7 +15388,8 @@ "@types/throttle-debounce": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/@types/throttle-debounce/-/throttle-debounce-2.1.0.tgz", - "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==" + "integrity": "sha512-5eQEtSCoESnh2FsiLTxE121IiE60hnMqcb435fShf4bpLRjEu1Eoekht23y6zXS9Ts3l+Szu3TARnTsA0GkOkQ==", + "dev": true }, "@types/tinycolor2": { "version": "1.4.3", @@ -16453,6 +16478,11 @@ "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001309.tgz", "integrity": "sha512-Pl8vfigmBXXq+/yUz1jUwULeq9xhMJznzdc/xwl4WclDAuebcTHVefpz8lE/bMI+UN7TOkSSe7B7RnZd6+dzjA==" }, + "canvas-confetti": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/canvas-confetti/-/canvas-confetti-1.5.1.tgz", + "integrity": "sha512-Ncz+oZJP6OvY7ti4E1slxVlyAV/3g7H7oQtcCDXgwGgARxPnwYY9PW5Oe+I8uvspYNtuHviAdgA0LfcKFWJfpg==" + }, "centra": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/centra/-/centra-1.0.1.tgz", diff --git a/frontend/package.json b/frontend/package.json index 633a75ff8..a00b3005a 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -17,6 +17,7 @@ "npm": "8.1.2" }, "devDependencies": { + "@types/canvas-confetti": "1.4.3", "@types/chartjs-plugin-trendline": "1.0.1", "@types/damerau-levenshtein": "1.0.0", "@types/grecaptcha": "3.0.4", @@ -55,6 +56,7 @@ }, "dependencies": { "axios": "0.21.4", + "canvas-confetti": "1.5.1", "chart.js": "3.7.1", "chartjs-adapter-date-fns": "2.0.0", "chartjs-plugin-annotation": "1.4.0", diff --git a/frontend/src/ts/commandline/lists/pace-caret.ts b/frontend/src/ts/commandline/lists/pace-caret.ts index 492da32ac..f71fcc0da 100644 --- a/frontend/src/ts/commandline/lists/pace-caret.ts +++ b/frontend/src/ts/commandline/lists/pace-caret.ts @@ -41,6 +41,15 @@ const subgroup: MonkeyTypes.CommandsSubgroup = { TestLogic.restart(); }, }, + { + id: "setPaceCaretDaily", + display: "daily", + configValue: "daily", + exec: (): void => { + UpdateConfig.setPaceCaret("daily"); + TestLogic.restart(); + }, + }, { id: "setPaceCaretCustom", display: "custom...", diff --git a/frontend/src/ts/config.ts b/frontend/src/ts/config.ts index 8dccbc5b3..802f98176 100644 --- a/frontend/src/ts/config.ts +++ b/frontend/src/ts/config.ts @@ -346,7 +346,7 @@ export function setPaceCaret( ): boolean { if ( !isConfigValueValid("pace caret", val, [ - ["custom", "off", "average", "pb", "last"], + ["custom", "off", "average", "pb", "last", "daily"], ]) ) { return false; diff --git a/frontend/src/ts/controllers/account-controller.ts b/frontend/src/ts/controllers/account-controller.ts index 5f738440c..40c5562c3 100644 --- a/frontend/src/ts/controllers/account-controller.ts +++ b/frontend/src/ts/controllers/account-controller.ts @@ -608,30 +608,6 @@ async function signUp(): Promise { return; } - // Force user to use a capital letter, number, special character when setting up an account and changing password - if (password.length < 8) { - Notifications.add("Password must be at least 8 characters", 0, 3); - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.updateSignupButton(); - return; - } - - const hasCapital = password.match(/[A-Z]/); - const hasNumber = password.match(/[\d]/); - const hasSpecial = password.match(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/); - if (!hasCapital || !hasNumber || !hasSpecial) { - Notifications.add( - "Password must contain at least one capital letter, number, and special character", - 0, - 3 - ); - LoginPage.hidePreloader(); - LoginPage.enableInputs(); - LoginPage.updateSignupButton(); - return; - } - if (password !== passwordVerify) { Notifications.add("Passwords do not match", 0, 3); LoginPage.hidePreloader(); @@ -640,6 +616,22 @@ async function signUp(): Promise { return; } + // Force user to use a capital letter, number, special character when setting up an account and changing password + if ( + window.location.hostname !== "localhost" && + !Misc.isPasswordStrong(password) + ) { + Notifications.add( + "Password must contain at least one capital letter, number, a special character and at least 8 characters long", + 0, + 4 + ); + LoginPage.hidePreloader(); + LoginPage.enableInputs(); + LoginPage.updateSignupButton(); + return; + } + authListener(); let createdAuthUser; @@ -688,8 +680,16 @@ async function signUp(): Promise { } catch (e) { //make sure to do clean up here if (createdAuthUser) { - await Ape.users.delete(); - await createdAuthUser.user.delete(); + try { + await Ape.users.delete(); + } catch (e) { + // account might already be deleted + } + try { + await createdAuthUser.user.delete(); + } catch (e) { + // account might already be deleted + } } console.log(e); const message = Misc.createErrorMessage(e, "Failed to create account"); diff --git a/frontend/src/ts/db.ts b/frontend/src/ts/db.ts index dbb04ac42..9c918fbdf 100644 --- a/frontend/src/ts/db.ts +++ b/frontend/src/ts/db.ts @@ -428,6 +428,65 @@ export async function getUserAverage10( return retval; } +export async function getUserDailyBest( + mode: M, + mode2: MonkeyTypes.Mode2, + punctuation: boolean, + language: string, + difficulty: MonkeyTypes.Difficulty, + lazyMode: boolean +): Promise { + const snapshot = getSnapshot(); + + if (!snapshot) return 0; + + function cont(): number { + const activeTagIds: string[] = []; + snapshot.tags?.forEach((tag) => { + if (tag.active === true) { + activeTagIds.push(tag._id); + } + }); + + let bestWpm = 0; + + if (snapshot.results !== undefined) { + for (const result of snapshot.results) { + if ( + result.mode === mode && + result.punctuation === punctuation && + result.language === language && + result.difficulty === difficulty && + (result.lazyMode === lazyMode || + (result.lazyMode === undefined && lazyMode === false)) && + (activeTagIds.length === 0 || + activeTagIds.some((tagId) => result.tags.includes(tagId))) + ) { + if (result.timestamp < Date.now() - 86400000) { + continue; + } + + // Continue if the mode2 doesn't match and it's not a quote + if (result.mode2 !== mode2 && mode !== "quote") { + continue; + } + + if (result.wpm > bestWpm) { + bestWpm = result.wpm; + } + } + } + } + + return bestWpm; + } + + const retval: number = + snapshot === null || (await getUserResults()) === null ? 0 : cont(); + + return retval; +} + export async function getLocalPB( mode: M, mode2: MonkeyTypes.Mode2, diff --git a/frontend/src/ts/elements/modes-notice.ts b/frontend/src/ts/elements/modes-notice.ts index 97398cd4a..f22b40b83 100644 --- a/frontend/src/ts/elements/modes-notice.ts +++ b/frontend/src/ts/elements/modes-notice.ts @@ -116,6 +116,8 @@ export async function update(): Promise { ? "pb" : Config.paceCaret === "last" ? "last" + : Config.paceCaret === "daily" + ? "daily" : "custom" } pace${speed}` ); diff --git a/frontend/src/ts/popups/simple-popups.ts b/frontend/src/ts/popups/simple-popups.ts index f029a22aa..e013a9767 100644 --- a/frontend/src/ts/popups/simple-popups.ts +++ b/frontend/src/ts/popups/simple-popups.ts @@ -20,6 +20,7 @@ import { unlink, updatePassword, } from "firebase/auth"; +import { isPasswordStrong } from "../utils/misc"; interface Input { placeholder?: string; @@ -524,6 +525,17 @@ list["updatePassword"] = new SimplePopup( Notifications.add("New passwords don't match", 0); return; } + if ( + window.location.hostname !== "localhost" && + !isPasswordStrong(newPass) + ) { + Notifications.add( + "New password must contain at least one capital letter, number, a special character and at least 8 characters long", + 0, + 4 + ); + return; + } Loader.show(); await reauthenticateWithCredential(user, credential); await updatePassword(user, newPass); diff --git a/frontend/src/ts/test/pace-caret.ts b/frontend/src/ts/test/pace-caret.ts index 7571f79f6..4643655a2 100644 --- a/frontend/src/ts/test/pace-caret.ts +++ b/frontend/src/ts/test/pace-caret.ts @@ -75,6 +75,16 @@ export async function init(): Promise { Config.lazyMode ); wpm = Math.round(wpm); + } else if (Config.paceCaret === "daily") { + wpm = await DB.getUserDailyBest( + Config.mode, + mode2, + Config.punctuation, + Config.language, + Config.difficulty, + Config.lazyMode + ); + wpm = Math.round(wpm); } else if (Config.paceCaret === "custom") { wpm = Config.paceCaretCustomSpeed; } else if (Config.paceCaret === "last" || TestState.isPaceRepeat == true) { diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 933cb213f..74ae71172 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -18,11 +18,13 @@ import * as AdController from "../controllers/ad-controller"; import * as TestConfig from "./test-config"; import { Chart } from "chart.js"; import { Auth } from "../firebase"; +import * as SlowTimer from "../states/slow-timer"; // eslint-disable-next-line no-duplicate-imports -- need to ignore because eslint doesnt know what import type is import type { PluginChartOptions, ScaleChartOptions } from "chart.js"; import type { AnnotationOptions } from "chartjs-plugin-annotation"; import Ape from "../ape"; +import confetti from "canvas-confetti"; let result: MonkeyTypes.Result; let maxChartVal: number; @@ -343,6 +345,38 @@ export function showCrown(): void { PbCrown.show(); } +export function showConfetti(): void { + if (SlowTimer.get()) return; + const style = getComputedStyle(document.body); + const colors = [ + style.getPropertyValue("--main-color"), + style.getPropertyValue("--text-color"), + style.getPropertyValue("--sub-color"), + ]; + const duration = Date.now() + 125; + + (function f(): void { + confetti({ + particleCount: 5, + angle: 60, + spread: 75, + origin: { x: 0 }, + colors: colors, + }); + confetti({ + particleCount: 5, + angle: 120, + spread: 75, + origin: { x: 1 }, + colors: colors, + }); + + if (Date.now() < duration) { + requestAnimationFrame(f); + } + })(); +} + export function hideCrown(): void { PbCrown.hide(); $("#result .stats .wpm .crown").attr("aria-label", ""); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 1aad2cf4b..542d7b964 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1738,6 +1738,11 @@ async function saveResult( if (response?.data?.isPb) { //new pb + if ( + DB.getSnapshot()?.personalBests?.[Config.mode]?.[completedEvent.mode2] + ) { + Result.showConfetti(); + } Result.showCrown(); Result.updateCrown(); DB.saveLocalPB( diff --git a/frontend/src/ts/types/types.d.ts b/frontend/src/ts/types/types.d.ts index 487276503..537c96ec6 100644 --- a/frontend/src/ts/types/types.d.ts +++ b/frontend/src/ts/types/types.d.ts @@ -108,7 +108,7 @@ declare namespace MonkeyTypes { type SoundVolume = "0.1" | "0.5" | "1.0"; - type PaceCaret = "off" | "average" | "pb" | "last" | "custom"; + type PaceCaret = "off" | "average" | "pb" | "last" | "custom" | "daily"; type PageWidth = "100" | "125" | "150" | "200" | "max"; diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 5cb31d492..b72fa1f3f 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -1235,3 +1235,11 @@ export function abbreviateNumber(num: number): string { export async function sleep(ms: number): Promise { return new Promise((resolve) => setTimeout(resolve, ms)); } + +export function isPasswordStrong(password: string): boolean { + const hasCapital = !!password.match(/[A-Z]/); + const hasNumber = !!password.match(/[\d]/); + const hasSpecial = !!password.match(/[!@#$%^&*()_+\-=[\]{};':"\\|,.<>/?]/); + const isLong = password.length >= 8; + return hasCapital && hasNumber && hasSpecial && isLong; +} diff --git a/frontend/static/email-handler.html b/frontend/static/email-handler.html index 28d09d84e..af66f1564 100644 --- a/frontend/static/email-handler.html +++ b/frontend/static/email-handler.html @@ -175,6 +175,16 @@