From 92add5b9277e19e6c3fdb3f142f84a30bda92452 Mon Sep 17 00:00:00 2001 From: Dominic Ruggiero Date: Fri, 24 Mar 2023 06:17:57 -0400 Subject: [PATCH 1/7] add mr.rogers quote (#4107) mrhappyma --- frontend/static/quotes/english.json | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/frontend/static/quotes/english.json b/frontend/static/quotes/english.json index 569cc3deb..7b65c38be 100644 --- a/frontend/static/quotes/english.json +++ b/frontend/static/quotes/english.json @@ -38812,6 +38812,12 @@ "source": "The World Was Wide Enough, Hamilton", "length": 81, "id": 6814 + }, + { + "text": "You are a very special person. There is only one like you in the whole world. There's never been anyone exactly like you before, and there will never be again. Only you. And people can like you exactly as you are.", + "source": "Fred Rogers", + "length": 213, + "id": 6815 } ] } From 8dd979fe94f2317444f592d41240998980c0bb2b Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 24 Mar 2023 11:38:08 +0100 Subject: [PATCH 2/7] fixed not smooth transition when always show history is enabled added no animation parameter removed duplicating code --- frontend/src/ts/test/result.ts | 5 +---- frontend/src/ts/test/test-ui.ts | 6 +++--- 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/test/result.ts b/frontend/src/ts/test/result.ts index 8a5a90253..a49244748 100644 --- a/frontend/src/ts/test/result.ts +++ b/frontend/src/ts/test/result.ts @@ -823,9 +823,6 @@ export async function update( $("#words").empty(); ChartController.result.resize(); - if (Config.alwaysShowWordsHistory && Config.burstHeatmap) { - TestUI.applyBurstHeatmap(); - } $("#result").trigger("focus"); window.scrollTo({ top: 0 }); $("#testModesNotice").addClass("hidden"); @@ -838,7 +835,7 @@ export async function update( 125 ); if (Config.alwaysShowWordsHistory && !GlarsesMode.get()) { - TestUI.toggleResultWords(); + TestUI.toggleResultWords(true); } Keymap.hide(); AdController.updateTestPageAds(true); diff --git a/frontend/src/ts/test/test-ui.ts b/frontend/src/ts/test/test-ui.ts index ca07878eb..cd3bb3717 100644 --- a/frontend/src/ts/test/test-ui.ts +++ b/frontend/src/ts/test/test-ui.ts @@ -985,7 +985,7 @@ async function loadWordsHistory(): Promise { return true; } -export function toggleResultWords(): void { +export function toggleResultWords(noAnimation = false): void { if (resultVisible) { if ($("#resultWordsHistory").stop(true, true).hasClass("hidden")) { //show @@ -1001,7 +1001,7 @@ export function toggleResultWords(): void { $("#resultWordsHistory") .removeClass("hidden") .css("display", "none") - .slideDown(250, () => { + .slideDown(noAnimation ? 0 : 250, () => { if (Config.burstHeatmap) { applyBurstHeatmap(); } @@ -1014,7 +1014,7 @@ export function toggleResultWords(): void { $("#resultWordsHistory") .removeClass("hidden") .css("display", "none") - .slideDown(250); + .slideDown(noAnimation ? 0 : 250); } } else { //hide From 03d42cef3375b7a1c00d8b34ce3747c635b97522 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 24 Mar 2023 12:15:54 +0100 Subject: [PATCH 3/7] added more data on hover closes #4109 --- frontend/src/ts/elements/profile.ts | 42 ++++++++++++++++++++++++----- frontend/src/ts/utils/misc.ts | 23 ++++++++++++++++ 2 files changed, 58 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/elements/profile.ts b/frontend/src/ts/elements/profile.ts index e192f6303..e6b579985 100644 --- a/frontend/src/ts/elements/profile.ts +++ b/frontend/src/ts/elements/profile.ts @@ -6,6 +6,7 @@ import { getHTMLById } from "../controllers/badge-controller"; import { throttle } from "throttle-debounce"; import * as EditProfilePopup from "../popups/edit-profile-popup"; import * as ActivePage from "../states/active-page"; +import { formatDistanceToNowStrict } from "date-fns/esm"; type ProfileViewPaths = "profile" | "account"; @@ -93,6 +94,8 @@ export async function update( const balloonText = `${diffDays} day${diffDays != 1 ? "s" : ""} ago`; details.find(".joined").text(joinedText).attr("aria-label", balloonText); + let hoverText = ""; + if (profile.streak && profile?.streak > 1) { details .find(".streak") @@ -100,17 +103,42 @@ export async function update( `Current streak: ${profile.streak} ${ profile.streak === 1 ? "day" : "days" }` - ) - .attr( - "aria-label", - `Longest streak: ${profile.maxStreak} ${ - profile.maxStreak === 1 ? "day" : "days" - }` ); + hoverText = `Longest streak: ${profile.maxStreak} ${ + profile.maxStreak === 1 ? "day" : "days" + }`; } else { - details.find(".streak").text("").attr("aria-label", ""); + details.find(".streak").text(""); + hoverText = ""; } + if (where === "account") { + const results = DB.getSnapshot()?.results; + const lastResult = results?.[0]; + + const dayInMilis = 1000 * 60 * 60 * 24; + const timeDif = formatDistanceToNowStrict( + Misc.getCurrentDayTimestamp() + dayInMilis + ); + + if (lastResult) { + //check if the last result is from today + const isToday = Misc.isToday(lastResult.timestamp); + if (isToday) { + hoverText += `\nClaimed today: yes`; + hoverText += `\nCome back in: ${timeDif}`; + } else { + hoverText += `\nClaimed today: no`; + hoverText += `\nStreak lost in: ${timeDif}`; + } + } + } + + details + .find(".streak") + .attr("aria-label", hoverText) + .attr("data-balloon-break", ""); + const typingStatsEl = details.find(".typingStats"); typingStatsEl .find(".started .value") diff --git a/frontend/src/ts/utils/misc.ts b/frontend/src/ts/utils/misc.ts index 298621e53..92dd02af0 100644 --- a/frontend/src/ts/utils/misc.ts +++ b/frontend/src/ts/utils/misc.ts @@ -1522,3 +1522,26 @@ export async function checkIfLanguageSupportsZipf( if (lang.orderedByFrequency === false) return "no"; return "unknown"; } + +export function getStartOfDayTimestamp(timestamp: number): number { + return timestamp - (timestamp % 86400000); +} + +export function getCurrentDayTimestamp(): number { + const currentTime = Date.now(); + return getStartOfDayTimestamp(currentTime); +} + +export function isYesterday(timestamp: number): boolean { + const yesterday = getStartOfDayTimestamp(Date.now() - 86400000); + const date = getStartOfDayTimestamp(timestamp); + + return yesterday === date; +} + +export function isToday(timestamp: number): boolean { + const today = getStartOfDayTimestamp(Date.now()); + const date = getStartOfDayTimestamp(timestamp); + + return today === date; +} From 317f5b23baf6ce0b21095395cc205f9006942945 Mon Sep 17 00:00:00 2001 From: Jack Date: Fri, 24 Mar 2023 12:17:27 +0100 Subject: [PATCH 4/7] Admin auth (#4101) * added admin auth * always decoding token --- backend/src/api/controllers/admin.ts | 5 +++++ backend/src/api/routes/admin.ts | 18 ++++++++++++++++++ backend/src/api/routes/index.ts | 2 ++ backend/src/dal/admin-uids.ts | 10 ++++++++++ backend/src/middlewares/api-utils.ts | 27 +++++++++++++++++++++++++++ backend/src/middlewares/auth.ts | 6 +++++- 6 files changed, 67 insertions(+), 1 deletion(-) create mode 100644 backend/src/api/controllers/admin.ts create mode 100644 backend/src/api/routes/admin.ts create mode 100644 backend/src/dal/admin-uids.ts diff --git a/backend/src/api/controllers/admin.ts b/backend/src/api/controllers/admin.ts new file mode 100644 index 000000000..0db718021 --- /dev/null +++ b/backend/src/api/controllers/admin.ts @@ -0,0 +1,5 @@ +import { MonkeyResponse } from "../../utils/monkey-response"; + +export async function test(): Promise { + return new MonkeyResponse("OK"); +} diff --git a/backend/src/api/routes/admin.ts b/backend/src/api/routes/admin.ts new file mode 100644 index 000000000..2fe6e0b96 --- /dev/null +++ b/backend/src/api/routes/admin.ts @@ -0,0 +1,18 @@ +// import joi from "joi"; +import { Router } from "express"; +import { authenticateRequest } from "../../middlewares/auth"; +import { asyncHandler, checkIfUserIsAdmin } from "../../middlewares/api-utils"; +import * as AdminController from "../controllers/admin"; + +const router = Router(); + +router.get( + "/", + authenticateRequest({ + noCache: true, + }), + checkIfUserIsAdmin(), + asyncHandler(AdminController.test) +); + +export default router; diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts index d577fd3d8..bca77442d 100644 --- a/backend/src/api/routes/index.ts +++ b/backend/src/api/routes/index.ts @@ -8,6 +8,7 @@ import configs from "./configs"; import results from "./results"; import presets from "./presets"; import apeKeys from "./ape-keys"; +import admin from "./admin"; import configuration from "./configuration"; import { version } from "../../version"; import leaderboards from "./leaderboards"; @@ -37,6 +38,7 @@ const API_ROUTE_MAP = { "/leaderboards": leaderboards, "/quotes": quotes, "/ape-keys": apeKeys, + "/admin": admin, }; function addApiRoutes(app: Application): void { diff --git a/backend/src/dal/admin-uids.ts b/backend/src/dal/admin-uids.ts new file mode 100644 index 000000000..cd2bbfaf5 --- /dev/null +++ b/backend/src/dal/admin-uids.ts @@ -0,0 +1,10 @@ +import * as db from "../init/db"; + +export async function isAdmin(uid: string): Promise { + const doc = await db.collection("admin-uids").findOne({ uid }); + if (doc) { + return true; + } else { + return false; + } +} diff --git a/backend/src/middlewares/api-utils.ts b/backend/src/middlewares/api-utils.ts index ccda2670e..52d280df6 100644 --- a/backend/src/middlewares/api-utils.ts +++ b/backend/src/middlewares/api-utils.ts @@ -4,6 +4,7 @@ import MonkeyError from "../utils/error"; import { Response, NextFunction, RequestHandler } from "express"; import { handleMonkeyResponse, MonkeyResponse } from "../utils/monkey-response"; import { getUser } from "../dal/user"; +import { isAdmin } from "../dal/admin-uids"; interface ValidationOptions { criteria: (data: T) => boolean; @@ -40,6 +41,31 @@ function validateConfiguration( }; } +/** + * Check if the user is an admin before handling request. + * Note that this middleware must be used after authentication in the middleware stack. + */ +function checkIfUserIsAdmin(): RequestHandler { + return async ( + req: MonkeyTypes.Request, + _res: Response, + next: NextFunction + ) => { + try { + const { uid } = req.ctx.decodedToken; + const admin = await isAdmin(uid); + + if (!admin) { + throw new MonkeyError(403, "You don't have permission to do this."); + } + } catch (error) { + next(error); + } + + next(); + }; +} + /** * Check user permissions before handling request. * Note that this middleware must be used after authentication in the middleware stack. @@ -158,6 +184,7 @@ function useInProduction(middlewares: RequestHandler[]): RequestHandler[] { export { validateConfiguration, checkUserPermissions, + checkIfUserIsAdmin, asyncHandler, validateRequest, useInProduction, diff --git a/backend/src/middlewares/auth.ts b/backend/src/middlewares/auth.ts index 39f23922b..6e98cc4c5 100644 --- a/backend/src/middlewares/auth.ts +++ b/backend/src/middlewares/auth.ts @@ -17,6 +17,7 @@ interface RequestAuthenticationOptions { isPublic?: boolean; acceptApeKeys?: boolean; requireFreshToken?: boolean; + noCache?: boolean; } const DEFAULT_OPTIONS: RequestAuthenticationOptions = { @@ -149,7 +150,10 @@ async function authenticateWithBearerToken( options: RequestAuthenticationOptions ): Promise { try { - const decodedToken = await verifyIdToken(token, options.requireFreshToken); + const decodedToken = await verifyIdToken( + token, + options.requireFreshToken || options.noCache + ); if (options.requireFreshToken) { const now = Date.now(); From 98b777dfcbc80ba3b5f542053a397aa797d48e06 Mon Sep 17 00:00:00 2001 From: Albert Date: Fri, 24 Mar 2023 07:21:18 -0400 Subject: [PATCH 5/7] Fix positions of pb and average lines when disabling start graph at 0 (#4103) albertying * Change accountHistory y-axis min of average and pb lines * extract the floor operation into a variable * remove whitespace * setting to 0 --------- Co-authored-by: Evan <64989416+Ferotiq@users.noreply.github.com> Co-authored-by: Evan Co-authored-by: Miodec --- frontend/src/ts/pages/account.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/pages/account.ts b/frontend/src/ts/pages/account.ts index 4360c6315..8e1dd59b9 100644 --- a/frontend/src/ts/pages/account.ts +++ b/frontend/src/ts/pages/account.ts @@ -798,9 +798,17 @@ function fillContent(): void { Math.floor(maxWpmChartVal) + (10 - (Math.floor(maxWpmChartVal) % 10)); if (!Config.startGraphsAtZero) { - accountHistoryScaleOptions["wpm"].min = Math.floor(minWpmChartVal); + const minWpmChartValFloor = Math.floor(minWpmChartVal); + + accountHistoryScaleOptions["wpm"].min = minWpmChartValFloor; + accountHistoryScaleOptions["pb"].min = minWpmChartValFloor; + accountHistoryScaleOptions["wpmAvgTen"].min = minWpmChartValFloor; + accountHistoryScaleOptions["wpmAvgHundred"].min = minWpmChartValFloor; } else { accountHistoryScaleOptions["wpm"].min = 0; + accountHistoryScaleOptions["pb"].min = 0; + accountHistoryScaleOptions["wpmAvgTen"].min = 0; + accountHistoryScaleOptions["wpmAvgHundred"].min = 0; } if (!chartData || chartData.length == 0) { From e6eed80b97f87dab23050e1a7a53df0554528135 Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 24 Mar 2023 12:26:06 +0100 Subject: [PATCH 6/7] important notifications closes #4099 --- frontend/src/ts/test/test-logic.ts | 26 +++++++++++++++++++------- 1 file changed, 19 insertions(+), 7 deletions(-) diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 862bbe019..12ffb28a5 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -881,13 +881,17 @@ export async function init(): Promise { } if (Config.tapeMode !== "off" && !language.leftToRight) { - Notifications.add("This language does not support tape mode.", 0); + Notifications.add("This language does not support tape mode.", 0, { + important: true, + }); UpdateConfig.setTapeMode("off"); } if (Config.lazyMode === true && language.noLazyMode) { rememberLazyMode = true; - Notifications.add("This language does not support lazy mode.", 0); + Notifications.add("This language does not support lazy mode.", 0, { + important: true, + }); UpdateConfig.setLazyMode(false, true); } else if (rememberLazyMode === true && !language.noLazyMode) { UpdateConfig.setLazyMode(true, true); @@ -1078,7 +1082,8 @@ export async function init(): Promise { `No ${Config.language .replace(/_\d*k$/g, "") .replace(/_/g, " ")} quotes found`, - 0 + 0, + { important: true } ); if (Auth?.currentUser) { QuoteSubmitPopup.show(false); @@ -1095,7 +1100,7 @@ export async function init(): Promise { ); if (targetQuote === undefined) { rq = quotesCollection.groups[0][0]; - Notifications.add("Quote Id Does Not Exist", 0); + Notifications.add("Quote Id Does Not Exist", 0, { important: true }); } else { rq = targetQuote; } @@ -1105,7 +1110,7 @@ export async function init(): Promise { ); if (randomQuote === null) { - Notifications.add("No favorite quotes found", 0); + Notifications.add("No favorite quotes found", 0, { important: true }); UpdateConfig.setQuoteLength(-1); restart(); return; @@ -1115,7 +1120,9 @@ export async function init(): Promise { } else { const randomQuote = QuotesController.getRandomQuote(); if (randomQuote === null) { - Notifications.add("No quotes found for selected quote length", 0); + Notifications.add("No quotes found for selected quote length", 0, { + important: true, + }); TestUI.setTestRestarting(false); return; } @@ -1312,6 +1319,7 @@ export async function retrySavingResult(): Promise { 0, { duration: 5, + important: true, } ); @@ -1668,6 +1676,7 @@ export async function finish(difficultyFailed = false): Promise { CustomText.setCustomTextLongProgress(customTextName, newProgress); Notifications.add("Long custom text progress saved", 1, { duration: 5, + important: true, }); let newText = CustomText.getCustomText(customTextName, true); @@ -1679,6 +1688,7 @@ export async function finish(difficultyFailed = false): Promise { CustomText.setText(CustomText.getCustomText(customTextName, true)); Notifications.add("Long custom text completed", 1, { duration: 5, + important: true, }); } } @@ -1762,6 +1772,7 @@ async function saveResult( Notifications.add("Result not saved: disabled by user", -1, { duration: 3, customTitle: "Notice", + important: true, }); AccountButton.loading(false); return; @@ -1771,6 +1782,7 @@ async function saveResult( Notifications.add("Result not saved: offline", -1, { duration: 2, customTitle: "Notice", + important: true, }); AccountButton.loading(false); retrySaving.canRetry = true; @@ -1892,7 +1904,7 @@ async function saveResult( $("#retrySavingResultButton").addClass("hidden"); if (isRetrying) { - Notifications.add("Result saved", 1); + Notifications.add("Result saved", 1, { important: true }); } } From 2d06bdce30f9da4a10dac94a150d27a17acbad4c Mon Sep 17 00:00:00 2001 From: Miodec Date: Fri, 24 Mar 2023 12:51:36 +0100 Subject: [PATCH 7/7] setting state when overriding custom text --- frontend/src/ts/controllers/challenge-controller.ts | 2 ++ frontend/src/ts/test/practise-words.ts | 1 + frontend/src/ts/test/test-logic.ts | 5 ++++- frontend/src/ts/utils/url-handler.ts | 3 +++ 4 files changed, 10 insertions(+), 1 deletion(-) diff --git a/frontend/src/ts/controllers/challenge-controller.ts b/frontend/src/ts/controllers/challenge-controller.ts index ac2ca74b4..cd5d7ea3d 100644 --- a/frontend/src/ts/controllers/challenge-controller.ts +++ b/frontend/src/ts/controllers/challenge-controller.ts @@ -231,6 +231,7 @@ export async function setup(challengeName: string): Promise { UpdateConfig.setMode("words", true); UpdateConfig.setDifficulty("normal", true); } else if (challenge.type === "customText") { + CustomText.setPopupTextareaState(challenge.parameters[0] as string); CustomText.setText((challenge.parameters[0] as string).split(" ")); CustomText.setIsTimeRandom(false); CustomText.setIsWordRandom(challenge.parameters[1] as boolean); @@ -249,6 +250,7 @@ export async function setup(challengeName: string): Promise { let text = scriptdata.trim(); text = text.replace(/[\n\r\t ]/gm, " "); text = text.replace(/ +/gm, " "); + CustomText.setPopupTextareaState(text); CustomText.setText(text.split(" ")); CustomText.setIsWordRandom(false); CustomText.setTime(-1); diff --git a/frontend/src/ts/test/practise-words.ts b/frontend/src/ts/test/practise-words.ts index 7af2473ee..c02a6464e 100644 --- a/frontend/src/ts/test/practise-words.ts +++ b/frontend/src/ts/test/practise-words.ts @@ -93,6 +93,7 @@ export function init(missed: boolean, slow: boolean): boolean { const numbers = before.numbers === null ? Config.numbers : before.numbers; UpdateConfig.setMode("custom", true); + CustomText.setPopupTextareaState(newCustomText.join(CustomText.delimiter)); CustomText.setText(newCustomText); CustomText.setIsWordRandom(true); CustomText.setIsTimeRandom(false); diff --git a/frontend/src/ts/test/test-logic.ts b/frontend/src/ts/test/test-logic.ts index 12ffb28a5..e9cf6744e 100644 --- a/frontend/src/ts/test/test-logic.ts +++ b/frontend/src/ts/test/test-logic.ts @@ -1681,11 +1681,14 @@ export async function finish(difficultyFailed = false): Promise { let newText = CustomText.getCustomText(customTextName, true); newText = newText.slice(newProgress); + CustomText.setPopupTextareaState(newText.join(CustomText.delimiter)); CustomText.setText(newText); } else { // They finished the test CustomText.setCustomTextLongProgress(customTextName, 0); - CustomText.setText(CustomText.getCustomText(customTextName, true)); + const text = CustomText.getCustomText(customTextName, true); + CustomText.setPopupTextareaState(text.join(CustomText.delimiter)); + CustomText.setText(text); Notifications.add("Long custom text completed", 1, { duration: 5, important: true, diff --git a/frontend/src/ts/utils/url-handler.ts b/frontend/src/ts/utils/url-handler.ts index 9b5c9c787..b869208b8 100644 --- a/frontend/src/ts/utils/url-handler.ts +++ b/frontend/src/ts/utils/url-handler.ts @@ -124,6 +124,9 @@ export function loadTestSettingsFromUrl(getOverride?: string): void { if (de[2]) { const customTextSettings = de[2]; + CustomText.setPopupTextareaState( + customTextSettings["text"].join(customTextSettings["delimiter"]) + ); CustomText.setText(customTextSettings["text"]); CustomText.setIsTimeRandom(customTextSettings["isTimeRandom"]); CustomText.setIsWordRandom(customTextSettings["isWordRandom"]);