From 5ae8ff07592b6f1c18906eb17c4ebd3dddf8cead Mon Sep 17 00:00:00 2001 From: Bruce Berrios <58147810+Bruception@users.noreply.github.com> Date: Sun, 6 Mar 2022 07:33:53 -0500 Subject: [PATCH 01/11] Remove quote languages check (#2645) --- backend/api/routes/quotes.ts | 6 +---- backend/constants/quote-languages.ts | 37 ---------------------------- 2 files changed, 1 insertion(+), 42 deletions(-) delete mode 100644 backend/constants/quote-languages.ts diff --git a/backend/api/routes/quotes.ts b/backend/api/routes/quotes.ts index 89ee10bd8..5fe0957d9 100644 --- a/backend/api/routes/quotes.ts +++ b/backend/api/routes/quotes.ts @@ -9,7 +9,6 @@ import { validateConfiguration, validateRequest, } from "../../middlewares/api-utils"; -import SUPPORTED_QUOTE_LANGUAGES from "../../constants/quote-languages"; const quotesRouter = Router(); @@ -119,10 +118,7 @@ quotesRouter.post( validateRequest({ body: { quoteId: joi.string().required(), - quoteLanguage: joi - .string() - .valid(...SUPPORTED_QUOTE_LANGUAGES) - .required(), + quoteLanguage: joi.string().required(), reason: joi .string() .valid( diff --git a/backend/constants/quote-languages.ts b/backend/constants/quote-languages.ts deleted file mode 100644 index a58356ae5..000000000 --- a/backend/constants/quote-languages.ts +++ /dev/null @@ -1,37 +0,0 @@ -const SUPPORTED_QUOTE_LANGUAGES = [ - "albanian", - "arabic", - "code_c++", - "code_c", - "code_java", - "code_javascript", - "code_python", - "code_rust", - "czech", - "danish", - "dutch", - "english", - "filipino", - "french", - "german", - "hindi", - "icelandic", - "indonesian", - "irish", - "italian", - "lithuanian", - "malagasy", - "polish", - "portuguese", - "russian", - "serbian", - "slovak", - "spanish", - "swedish", - "thai", - "toki_pona", - "turkish", - "vietnamese", -]; - -export default SUPPORTED_QUOTE_LANGUAGES; From 134389515ce2f5913e2a69a87031134efbd9df76 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 15:34:19 +0100 Subject: [PATCH 02/11] difficulty change clears active challenge --- frontend/src/scripts/controllers/challenge-controller.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/frontend/src/scripts/controllers/challenge-controller.ts b/frontend/src/scripts/controllers/challenge-controller.ts index ca45fc174..a40bee1c9 100644 --- a/frontend/src/scripts/controllers/challenge-controller.ts +++ b/frontend/src/scripts/controllers/challenge-controller.ts @@ -271,6 +271,7 @@ export async function setup(challengeName: string): Promise { ConfigEvent.subscribe((eventKey) => { if ( [ + "difficulty", "numbers", "punctuation", "mode", From f221326f4783f91027572141e33d353a60eb8731 Mon Sep 17 00:00:00 2001 From: Jack Date: Sun, 6 Mar 2022 16:34:48 +0100 Subject: [PATCH 03/11] Added Ape Keys Popup (#2642) * added disabled field to simple popup input * inverted disabled logic * fixed initial value completely not working in simple popups * loading ape keys into the snapshot * added type for ape keys * only vertical resize for textareas * added before init function added can close parameter correclty handling textearas * storing active popup in a variable * fixed click handler * hiding text element if string is empty * updated ape keys types * added click handler to open ape keys popup * added simple poopups for generating, editing and deleting ape keys * added ape keys popup * updated ape key type * ape keys is optional * not getting ape keys by default * added function to get ape keys * refactor * using last used on property --- .../src/scripts/ape/endpoints/ape-keys.ts | 5 +- frontend/src/scripts/ape/types/ape.d.ts | 3 +- frontend/src/scripts/db.ts | 22 ++ frontend/src/scripts/pages/settings.ts | 13 +- frontend/src/scripts/popups/ape-keys-popup.ts | 127 +++++++++ frontend/src/scripts/popups/simple-popups.ts | 244 +++++++++++++++++- frontend/src/scripts/types/types.d.ts | 13 + frontend/src/styles/inputs.scss | 4 + frontend/src/styles/popups.scss | 106 ++++++++ frontend/static/index.html | 40 +++ 10 files changed, 557 insertions(+), 20 deletions(-) create mode 100644 frontend/src/scripts/popups/ape-keys-popup.ts diff --git a/frontend/src/scripts/ape/endpoints/ape-keys.ts b/frontend/src/scripts/ape/endpoints/ape-keys.ts index aa17b10b4..660d5a7bd 100644 --- a/frontend/src/scripts/ape/endpoints/ape-keys.ts +++ b/frontend/src/scripts/ape/endpoints/ape-keys.ts @@ -14,10 +14,9 @@ export default function getApeKeysEndpoints( async function update( apeKeyId: string, - name: string, - enabled: boolean + updates: { name?: string; enabled?: boolean } ): Ape.EndpointData { - const payload = { name, enabled }; + const payload = { ...updates }; return await apeClient.patch(`${BASE_PATH}/${apeKeyId}`, { payload }); } diff --git a/frontend/src/scripts/ape/types/ape.d.ts b/frontend/src/scripts/ape/types/ape.d.ts index b1315f844..e3dc287cf 100644 --- a/frontend/src/scripts/ape/types/ape.d.ts +++ b/frontend/src/scripts/ape/types/ape.d.ts @@ -136,8 +136,7 @@ declare namespace Ape { generate: (name: string, enabled: boolean) => EndpointData; update: ( apeKeyId: string, - name: string, - enabled: boolean + updates: { name?: string; enabled?: boolean } ) => EndpointData; delete: (apeKeyId: string) => EndpointData; }; diff --git a/frontend/src/scripts/db.ts b/frontend/src/scripts/db.ts index c52ea24c3..fdf0a88f1 100644 --- a/frontend/src/scripts/db.ts +++ b/frontend/src/scripts/db.ts @@ -182,6 +182,28 @@ export async function getUserResults(): Promise { return true; } } + +export async function getUserApeKeys(): Promise< + MonkeyTypes.ApeKeys | undefined +> { + const user = firebase.auth().currentUser; + if (user == null) return undefined; + if (dbSnapshot === null) return undefined; + if (dbSnapshot.apeKeys !== undefined) { + return dbSnapshot.apeKeys; + } else { + const response = await Ape.apeKeys.get(); + + if (response.status !== 200) { + Notifications.add("Error getting ape keys: " + response.message, -1); + return undefined; + } + + dbSnapshot.apeKeys = response.data as MonkeyTypes.ApeKeys; + return dbSnapshot.apeKeys; + } +} + export async function getUserHighestWpm( mode: M, mode2: MonkeyTypes.Mode2, diff --git a/frontend/src/scripts/pages/settings.ts b/frontend/src/scripts/pages/settings.ts index e4fe7e831..9b0fde931 100644 --- a/frontend/src/scripts/pages/settings.ts +++ b/frontend/src/scripts/pages/settings.ts @@ -12,6 +12,7 @@ import * as ImportExportSettingsPopup from "../popups/import-export-settings-pop import * as CustomThemePopup from "../popups/custom-theme-popup"; import * as ConfigEvent from "../observables/config-event"; import * as ActivePage from "../states/active-page"; +import * as ApeKeysPopup from "../popups/ape-keys-popup"; import Page from "./page"; type SettingsGroups = { @@ -948,10 +949,14 @@ $("#shareCustomThemeButton").click(() => { ); }); -$(".pageSettings .sectionGroupTitle").click((e) => { +$(".pageSettings .sectionGroupTitle").on("click", (e) => { toggleSettingsGroup($(e.currentTarget).attr("group") as string); }); +$(".pageSettings .section.apeKeys #showApeKeysPopup").on("click", () => { + ApeKeysPopup.show(); +}); + $(".pageSettings .section.customBackgroundSize .inputAndButton .save").on( "click", () => { @@ -982,7 +987,7 @@ $(".pageSettings .section.customLayoutfluid .inputAndButton .save").on( $( ".pageSettings .section.customLayoutfluid .inputAndButton input" ).val() as MonkeyTypes.CustomLayoutFluidSpaces - ).then(bool => { + ).then((bool) => { if (bool) { Notifications.add("Custom layoutfluid saved", 1); } @@ -997,8 +1002,8 @@ $(".pageSettings .section.customLayoutfluid .inputAndButton .input").keypress( $( ".pageSettings .section.customLayoutfluid .inputAndButton input" ).val() as MonkeyTypes.CustomLayoutFluidSpaces - ).then(bool => { - if (bool) { + ).then((bool) => { + if (bool) { Notifications.add("Custom layoutfluid saved", 1); } }); diff --git a/frontend/src/scripts/popups/ape-keys-popup.ts b/frontend/src/scripts/popups/ape-keys-popup.ts new file mode 100644 index 000000000..255339168 --- /dev/null +++ b/frontend/src/scripts/popups/ape-keys-popup.ts @@ -0,0 +1,127 @@ +import * as DB from "../db"; +import Ape from "../ape"; +import * as Loader from "../elements/loader"; +import * as Notifications from "../elements/notifications"; + +function refreshList(): void { + const data = DB.getSnapshot().apeKeys; + if (!data) return; + const table = $("#apeKeysPopupWrapper table tbody"); + table.empty(); + const apeKeyIds = Object.keys(data); + if (apeKeyIds.length === 0) { + table.append( + "No keys found" + ); + return; + } + apeKeyIds.forEach((apeKeyId) => { + const key = data[apeKeyId] as MonkeyTypes.ApeKey; + table.append(` + + +
+ ${ + key.enabled + ? `` + : `` + } +
+ + ${key.name} + ${moment(key.createdOn).format("DD MMM YYYY HH:mm")} + ${moment(key.modifiedOn).format("DD MMM YYYY HH:mm")} + ${ + key.lastUsedOn === -1 + ? "-" + : moment(key.lastUsedOn).format("DD MMM YYYY HH:mm") + } + +
+
+ +
+
+ +
+
+ + + `); + }); +} + +export function hide(): void { + if (!$("#apeKeysPopupWrapper").hasClass("hidden")) { + $("#apeKeysPopupWrapper") + .stop(true, true) + .css("opacity", 1) + .animate( + { + opacity: 0, + }, + 100, + () => { + $("#apeKeysPopupWrapper").addClass("hidden"); + } + ); + } +} + +//show the popup +export async function show(): Promise { + if ($("#apeKeysPopupWrapper").hasClass("hidden")) { + Loader.show(); + await DB.getUserApeKeys(); + Loader.hide(); + refreshList(); + $("#apeKeysPopupWrapper") + .stop(true, true) + .css("opacity", 0) + .removeClass("hidden") + .animate( + { + opacity: 1, + }, + 100, + () => { + $("#apeKeysPopup textarea").focus().select(); + } + ); + } +} + +$("#apeKeysPopupWrapper").on("mousedown", (e) => { + if ($(e.target).attr("id") === "apeKeysPopupWrapper") { + hide(); + } +}); + +$("#apeKeysPopup .generateApeKey").on("click", () => { + hide(); +}); + +$(document).on("click", "#apeKeysPopup table .keyButtons .button", () => { + hide(); +}); + +$(document).on("click", "#apeKeysPopup table .icon-button", async (e) => { + const keyId = $(e.target).closest("tr").attr("keyId") as string; + const snap = DB.getSnapshot(); + const key = snap.apeKeys?.[keyId]; + if (!key || !snap.apeKeys) return; + Loader.show(); + const response = await Ape.apeKeys.update(keyId, { enabled: !key.enabled }); + Loader.hide(); + if (response.status !== 200) { + return Notifications.add("Failed to update key: " + response.message, -1); + } + snap.apeKeys[keyId].enabled = !key.enabled; + DB.setSnapshot(snap); + refreshList(); + if (key.enabled) { + Notifications.add("Key active", 1); + } else { + Notifications.add("Key inactive", 1); + } +}); diff --git a/frontend/src/scripts/popups/simple-popups.ts b/frontend/src/scripts/popups/simple-popups.ts index c44abdb81..91e8f69da 100644 --- a/frontend/src/scripts/popups/simple-popups.ts +++ b/frontend/src/scripts/popups/simple-popups.ts @@ -6,14 +6,18 @@ import * as UpdateConfig from "../config"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; import * as Settings from "../pages/settings"; +import * as ApeKeysPopup from "../popups/ape-keys-popup"; type Input = { placeholder: string; type?: string; initVal: string; hidden?: boolean; + disabled?: boolean; }; +let activePopup: SimplePopup | null = null; + export const list: { [key: string]: SimplePopup } = {}; class SimplePopup { parameters: string[]; @@ -26,7 +30,9 @@ class SimplePopup { text: string; buttonText: string; execFn: (thisPopup: SimplePopup, ...params: string[]) => void | Promise; + beforeInitFn: (thisPopup: SimplePopup) => void; beforeShowFn: (thisPopup: SimplePopup) => void; + canClose: boolean; constructor( id: string, type: string, @@ -38,6 +44,7 @@ class SimplePopup { thisPopup: SimplePopup, ...params: string[] ) => void | Promise, + beforeInitFn: (thisPopup: SimplePopup) => void, beforeShowFn: (thisPopup: SimplePopup) => void ) { this.parameters = []; @@ -51,7 +58,9 @@ class SimplePopup { this.wrapper = $("#simplePopupWrapper"); this.element = $("#simplePopup"); this.buttonText = buttonText; + this.beforeInitFn = (thisPopup): void => beforeInitFn(thisPopup); this.beforeShowFn = (thisPopup): void => beforeShowFn(thisPopup); + this.canClose = true; } reset(): void { this.element.html(` @@ -78,6 +87,12 @@ class SimplePopup { el.find(".button").text(this.buttonText); } + if (this.text === "") { + el.find(".text").addClass("hidden"); + } else { + el.find(".text").removeClass("hidden"); + } + // } } @@ -90,7 +105,7 @@ class SimplePopup { { if (input.type) { - el.find(".inputs").append(` + if (input.type === "textarea") { + el.find(".inputs").append(` + + `); + } else { + el.find(".inputs").append(` - `); + `); + } } else { el.find(".inputs").append(` `); @@ -132,6 +161,7 @@ class SimplePopup { } exec(): void { + if (!this.canClose) return; const vals: string[] = []; $.each($("#simplePopup input"), (_, el) => { vals.push($(el).val() as string); @@ -141,9 +171,11 @@ class SimplePopup { } show(parameters: string[] = []): void { + activePopup = this; this.parameters = parameters; - this.beforeShowFn(this); + this.beforeInitFn(this); this.init(); + this.beforeShowFn(this); this.wrapper .stop(true, true) .css("opacity", 0) @@ -154,6 +186,8 @@ class SimplePopup { } hide(): void { + if (!this.canClose) return; + activePopup = null; this.wrapper .stop(true, true) .css("opacity", 1) @@ -165,6 +199,7 @@ class SimplePopup { } export function hide(): void { + if (activePopup) return activePopup.hide(); $("#simplePopupWrapper") .stop(true, true) .css("opacity", 1) @@ -176,6 +211,7 @@ export function hide(): void { $("#simplePopupWrapper").mousedown((e) => { if ($(e.target).attr("id") === "simplePopupWrapper") { + if (activePopup) return activePopup.hide(); $("#simplePopupWrapper") .stop(true, true) .css("opacity", 1) @@ -266,6 +302,9 @@ list["updateEmail"] = new SimplePopup( thisPopup.buttonText = ""; thisPopup.text = "Password authentication is not enabled"; } + }, + (_thisPopup) => { + // } ); @@ -338,6 +377,9 @@ list["updateName"] = new SimplePopup( thisPopup.inputs[0].hidden = true; thisPopup.buttonText = "Reauthenticate to update"; } + }, + (_thisPopup) => { + // } ); @@ -400,6 +442,9 @@ list["updatePassword"] = new SimplePopup( thisPopup.buttonText = ""; thisPopup.text = "Password authentication is not enabled"; } + }, + (_thisPopup) => { + // } ); @@ -449,6 +494,9 @@ list["addPasswordAuth"] = new SimplePopup( }, () => { // + }, + (_thisPopup) => { + // } ); @@ -526,6 +574,9 @@ list["deleteAccount"] = new SimplePopup( thisPopup.inputs = []; thisPopup.buttonText = "Reauthenticate to delete"; } + }, + (_thisPopup) => { + // } ); @@ -569,6 +620,9 @@ list["clearTagPb"] = new SimplePopup( }, (thisPopup) => { thisPopup.text = `Are you sure you want to clear PB for tag ${thisPopup.parameters[1]}?`; + }, + (_thisPopup) => { + // } ); @@ -585,6 +639,9 @@ list["applyCustomFont"] = new SimplePopup( }, () => { // + }, + (_thisPopup) => { + // } ); @@ -643,6 +700,9 @@ list["resetPersonalBests"] = new SimplePopup( thisPopup.inputs = []; thisPopup.buttonText = "Reauthenticate to reset"; } + }, + (_thisPopup) => { + // } ); @@ -661,6 +721,9 @@ list["resetSettings"] = new SimplePopup( }, () => { // + }, + (_thisPopup) => { + // } ); @@ -689,6 +752,151 @@ list["unlinkDiscord"] = new SimplePopup( }, () => { // + }, + (_thisPopup) => { + // + } +); + +list["generateApeKey"] = new SimplePopup( + "generateApeKey", + "text", + "Generate new key", + [ + { + placeholder: "Name", + initVal: "", + }, + ], + "", + "Generate", + async (_thisPopup, name) => { + Loader.show(); + const response = await Ape.apeKeys.generate(name, false); + Loader.hide(); + + if (response.status !== 200) { + return Notifications.add( + "Failed to generate key: " + response.message, + -1 + ); + } else { + const data = response.data; + list["viewApeKey"].show([data.apeKey]); + const snap = DB.getSnapshot(); + if (snap.apeKeys) { + snap.apeKeys[data.apeKeyId] = data.apeKeyDetails; + DB.setSnapshot(snap); + } + } + }, + () => { + // + }, + (_thisPopup) => { + // + } +); + +list["viewApeKey"] = new SimplePopup( + "viewApeKey", + "text", + "Ape Key", + [ + { + type: "textarea", + disabled: true, + placeholder: "Key", + initVal: "", + }, + ], + "This is your new Ape Key. Please keep it safe. You will only see it once!", + "Close", + (_thisPopup) => { + ApeKeysPopup.show(); + }, + (_thisPopup) => { + _thisPopup.inputs[0].initVal = _thisPopup.parameters[0]; + }, + (_thisPopup) => { + _thisPopup.canClose = false; + $("#simplePopup textarea").css("height", "110px"); + $("#simplePopup .button").addClass("hidden"); + setTimeout(() => { + _thisPopup.canClose = true; + $("#simplePopup .button").removeClass("hidden"); + }, 3000); + } +); + +list["deleteApeKey"] = new SimplePopup( + "deleteApeKey", + "text", + "Delete Ape Key", + [], + "Are you sure?", + "Delete", + async (_thisPopup) => { + Loader.show(); + const response = await Ape.apeKeys.delete(_thisPopup.parameters[0]); + Loader.hide(); + + if (response.status !== 200) { + return Notifications.add("Failed to delete key: " + response.message, -1); + } + + Notifications.add("Key deleted", 1); + const snap = DB.getSnapshot(); + if (snap.apeKeys) { + delete snap.apeKeys[_thisPopup.parameters[0]]; + DB.setSnapshot(snap); + } + ApeKeysPopup.show(); + }, + (_thisPopup) => { + // + }, + (_thisPopup) => { + // + } +); + +list["editApeKey"] = new SimplePopup( + "editApeKey", + "text", + "Edit Ape Key", + [ + { + placeholder: "Name", + initVal: "", + }, + ], + "", + "Edit", + async (_thisPopup, input) => { + Loader.show(); + const response = await Ape.apeKeys.update(_thisPopup.parameters[0], { + name: input, + }); + Loader.hide(); + + if (response.status !== 200) { + return Notifications.add("Failed to update key: " + response.message, -1); + } + + Notifications.add("Key updated", 1); + const snap = DB.getSnapshot(); + if (snap.apeKeys) { + snap.apeKeys[_thisPopup.parameters[0]].name = input; + DB.setSnapshot(snap); + } + ApeKeysPopup.show(); + }, + (_thisPopup) => { + // + }, + (_thisPopup) => { + // } ); @@ -726,6 +934,20 @@ $(".pageSettings #deleteAccount").on("click", () => { list["deleteAccount"].show(); }); +$("#apeKeysPopup .generateApeKey").on("click", () => { + list["generateApeKey"].show(); +}); + +$(document).on("click", "#apeKeysPopup table tbody tr .button.delete", (e) => { + const keyId = $(e.target).closest("tr").attr("keyId") as string; + list["deleteApeKey"].show([keyId]); +}); + +$(document).on("click", "#apeKeysPopup table tbody tr .button.edit", (e) => { + const keyId = $(e.target).closest("tr").attr("keyId") as string; + list["editApeKey"].show([keyId]); +}); + $(document).on( "click", ".pageSettings .section.fontFamily .button.custom", diff --git a/frontend/src/scripts/types/types.d.ts b/frontend/src/scripts/types/types.d.ts index e50b328dd..afd9c5898 100644 --- a/frontend/src/scripts/types/types.d.ts +++ b/frontend/src/scripts/types/types.d.ts @@ -241,6 +241,18 @@ declare namespace MonkeyTypes { hash?: string; } + type ApeKey = { + name: string; + enabled: boolean; + createdOn: number; + modifiedOn: number; + lastUsedOn: number; + }; + + interface ApeKeys { + [key: string]: ApeKey; + } + interface Config { theme: string; themeLight: string; @@ -403,6 +415,7 @@ declare namespace MonkeyTypes { quoteMod?: boolean; discordId?: string; config?: Config; + apeKeys?: ApeKeys; } type PartialRecord = { diff --git a/frontend/src/styles/inputs.scss b/frontend/src/styles/inputs.scss index a60fac5a4..55f9a4dad 100644 --- a/frontend/src/styles/inputs.scss +++ b/frontend/src/styles/inputs.scss @@ -10,6 +10,10 @@ textarea { font-family: var(--font); } +textarea { + resize: vertical; +} + input[type="range"] { -webkit-appearance: none; padding: 0; diff --git a/frontend/src/styles/popups.scss b/frontend/src/styles/popups.scss index 5fc36c310..9f8fedd8f 100644 --- a/frontend/src/styles/popups.scss +++ b/frontend/src/styles/popups.scss @@ -522,6 +522,112 @@ } } +#apeKeysPopup { + background: var(--bg-color); + border-radius: var(--roundness); + padding: 2rem; + display: grid; + gap: 1rem; + width: 1000px; + max-width: calc(100vw - 4rem); + // height: 100%; + // max-height: 40rem; + min-height: 18rem; + overflow-y: scroll; + grid-template-rows: max-content auto; + align-items: baseline; + gap: 1rem; + + .top { + display: grid; + grid-template-columns: 1fr auto; + .title { + font-size: 1.5rem; + color: var(--sub-color); + } + .button { + padding: 0.4rem 2rem; + } + } + + .icon-button { + justify-content: center; + } + + .keyButtons { + display: grid; + grid-auto-flow: column; + gap: 1rem; + .button { + width: 3rem; + } + } + + table { + width: 100%; + border-spacing: 0; + border-collapse: collapse; + + tr td:first-child { + text-align: center; + } + + tr.me { + td { + color: var(--main-color); + // font-weight: 900; + } + } + + td { + padding: 0.5rem 0.5rem; + } + + thead { + color: var(--sub-color); + font-size: 0.75rem; + + td { + padding: 0.5rem; + background: var(--bg-color); + position: -webkit-sticky; + position: sticky; + top: 0; + z-index: 99; + } + } + + tbody { + color: var(--text-color); + + tr:nth-child(odd) td { + background: rgba(0, 0, 0, 0.1); + } + } + + tfoot { + td { + padding: 1rem 0.5rem; + position: -webkit-sticky; + position: sticky; + bottom: -5px; + background: var(--bg-color); + color: var(--main-color); + z-index: 4; + } + } + + tr { + td:first-child { + padding-left: 1rem; + } + td:last-child { + padding-right: 1rem; + } + } + } +} + #quoteApprovePopup { background: var(--bg-color); border-radius: var(--roundness); diff --git a/frontend/static/index.html b/frontend/static/index.html index c0b3c2c8d..08f887c22 100644 --- a/frontend/static/index.html +++ b/frontend/static/index.html @@ -551,6 +551,30 @@
Submit
+ +
+

ape keys

+
+ Generate Ape Keys to access certain API endpoints. +
+
+
+ open +
+
+

reset settings

From 16a982e71e70375af50ec3bc4538e7b5054c3658 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 17:16:33 +0100 Subject: [PATCH 04/11] added decoded token types --- backend/middlewares/auth.ts | 3 +++ backend/types/types.d.ts | 1 + 2 files changed, 4 insertions(+) diff --git a/backend/middlewares/auth.ts b/backend/middlewares/auth.ts index 2c4d3d304..9e70990dd 100644 --- a/backend/middlewares/auth.ts +++ b/backend/middlewares/auth.ts @@ -75,6 +75,7 @@ function authenticateWithBody( } return { + type: "Bearer", uid, }; } @@ -110,6 +111,7 @@ async function authenticateWithBearerToken( const decodedToken = await verifyIdToken(token); return { + type: "Bearer", uid: decodedToken.uid, email: decodedToken.email, }; @@ -169,6 +171,7 @@ async function authenticateWithApeKey( await ApeKeysDAO.updateLastUsedOn(keyOwner, keyId); return { + type: "ApeKey", uid, email: keyOwner.email, }; diff --git a/backend/types/types.d.ts b/backend/types/types.d.ts index 269c2d976..906ea829b 100644 --- a/backend/types/types.d.ts +++ b/backend/types/types.d.ts @@ -27,6 +27,7 @@ declare namespace MonkeyTypes { } interface DecodedToken { + type?: "Bearer" | "ApeKey"; uid?: string; email?: string; } From 08d7ded235215b4b966b76d17f55cd0c1437ec79 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 18:13:58 +0100 Subject: [PATCH 05/11] changed error code --- backend/api/controllers/ape-keys.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/backend/api/controllers/ape-keys.ts b/backend/api/controllers/ape-keys.ts index ab78340fb..42702cfbf 100644 --- a/backend/api/controllers/ape-keys.ts +++ b/backend/api/controllers/ape-keys.ts @@ -32,7 +32,7 @@ class ApeKeysController { if (currentNumberOfApeKeys >= maxKeysPerUser) { throw new MonkeyError( - 500, + 400, "Maximum number of ApeKeys have been generated" ); } From 83c1e8adf56ad6528fcf39c57980a7aca8fd14ac Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 18:33:20 +0100 Subject: [PATCH 06/11] added rate limiting for ape key endpoints --- backend/middlewares/ape-rate-limit.ts | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) create mode 100644 backend/middlewares/ape-rate-limit.ts diff --git a/backend/middlewares/ape-rate-limit.ts b/backend/middlewares/ape-rate-limit.ts new file mode 100644 index 000000000..582cf3c5b --- /dev/null +++ b/backend/middlewares/ape-rate-limit.ts @@ -0,0 +1,31 @@ +import { Response, NextFunction } from "express"; +import rateLimit, { Options } from "express-rate-limit"; +import MonkeyError from "../utils/error"; + +const REQUEST_MULTIPLIER = process.env.MODE === "dev" ? 100 : 1; + +const getKey = (req: MonkeyTypes.Request, _res: Response): string => { + return req?.ctx?.decodedToken?.uid; +}; + +const customHandler = ( + _req: MonkeyTypes.Request, + _res: Response, + _next: NextFunction, + _options: Options +): void => { + throw new MonkeyError(429, "Too many attempts, please try again later."); +}; + +const ONE_MINUTE = 1000 * 60; + +export default rateLimit({ + windowMs: ONE_MINUTE, + max: 30 * REQUEST_MULTIPLIER, + keyGenerator: getKey, + handler: customHandler, + skip: (req: MonkeyTypes.Request, _res) => { + const decodedToken = req?.ctx?.decodedToken; + return decodedToken?.type !== "ApeKey"; + }, +}); From 8af10af5999dfe52b68cce39da1225716f028b0d Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 18:33:36 +0100 Subject: [PATCH 07/11] added enpoints for getting personal bests --- backend/api/controllers/user.ts | 10 ++++++++++ backend/api/routes/users.ts | 23 +++++++++++++++++++++++ backend/dao/user.js | 9 +++++++++ 3 files changed, 42 insertions(+) diff --git a/backend/api/controllers/user.ts b/backend/api/controllers/user.ts index 2fd0c9d3c..f78adb602 100644 --- a/backend/api/controllers/user.ts +++ b/backend/api/controllers/user.ts @@ -210,6 +210,16 @@ class UserController { await UsersDAO.updateLbMemory(uid, mode, mode2, language, rank); return new MonkeyResponse("Leaderboard memory updated"); } + + static async getPersonalBests( + req: MonkeyTypes.Request + ): Promise { + const { uid } = req.ctx.decodedToken; + const { mode, mode2 } = req.params; + + const data = (await UsersDAO.getPersonalBests(uid, mode, mode2)) ?? null; + return new MonkeyResponse("Personal bests retrieved", data); + } } export default UserController; diff --git a/backend/api/routes/users.ts b/backend/api/routes/users.ts index 34d83cae2..fa1128de1 100644 --- a/backend/api/routes/users.ts +++ b/backend/api/routes/users.ts @@ -4,6 +4,7 @@ import { Router } from "express"; import UserController from "../controllers/user"; import { asyncHandler, validateRequest } from "../../middlewares/api-utils"; import * as RateLimit from "../../middlewares/rate-limit"; +import ApeRateLimit from "../../middlewares/ape-rate-limit"; import { isUsernameValid } from "../../utils/validation"; const router = Router(); @@ -200,4 +201,26 @@ router.post( asyncHandler(UserController.unlinkDiscord) ); +router.get( + "/personalBests/:mode/", + RateLimit.userGet, + authenticateRequest({ + isPublic: false, + acceptApeKeys: true, + }), + ApeRateLimit, + asyncHandler(UserController.getPersonalBests) +); + +router.get( + "/personalBests/:mode/:mode2", + RateLimit.userGet, + authenticateRequest({ + isPublic: false, + acceptApeKeys: true, + }), + ApeRateLimit, + asyncHandler(UserController.getPersonalBests) +); + export default router; diff --git a/backend/dao/user.js b/backend/dao/user.js index 7e8a78513..b105c5738 100644 --- a/backend/dao/user.js +++ b/backend/dao/user.js @@ -301,6 +301,15 @@ class UsersDAO { static async setApeKeys(uid, apeKeys) { await db.collection("users").updateOne({ uid }, { $set: { apeKeys } }); } + + 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; From f8cc82cd23107a9be8078201e40ff1ea67308a8f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 18:36:25 +0100 Subject: [PATCH 08/11] always rounding to 2 after calculations are done --- frontend/src/scripts/test/test-stats.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/frontend/src/scripts/test/test-stats.ts b/frontend/src/scripts/test/test-stats.ts index c31cbae34..2939f176a 100644 --- a/frontend/src/scripts/test/test-stats.ts +++ b/frontend/src/scripts/test/test-stats.ts @@ -397,8 +397,8 @@ function countChars(): CharCount { export function calculateStats(): Stats { let testSeconds = calculateTestSeconds(); - console.log((end2 - start2) / 1000); - console.log(testSeconds); + // console.log((end2 - start2) / 1000); + // console.log(testSeconds); if (Config.mode != "custom") { testSeconds = Misc.roundTo2(testSeconds); } @@ -428,7 +428,7 @@ export function calculateStats(): Stats { chars.spaces + chars.incorrectChars + chars.extraChars, - time: testSeconds, + time: Misc.roundTo2(testSeconds), spaces: chars.spaces, correctSpaces: chars.correctSpaces, }; From 8049cb661d2e1901068d88636f21b99af16c5d7b Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 18:37:02 +0100 Subject: [PATCH 09/11] hiding ape keys for now --- frontend/static/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/static/index.html b/frontend/static/index.html index 08f887c22..c73034290 100644 --- a/frontend/static/index.html +++ b/frontend/static/index.html @@ -4139,7 +4139,7 @@
-
+

reset settings

From bd0c2b9026c5da9f1a6ba4f6755acaabb085898f Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 23:42:22 +0100 Subject: [PATCH 10/11] not storing ape keys in snapshot, redownloading when opening the ape keys popup to get latest dates --- frontend/src/scripts/db.ts | 21 ------------- frontend/src/scripts/popups/ape-keys-popup.ts | 30 ++++++++++++------- frontend/src/scripts/types/types.d.ts | 1 - 3 files changed, 20 insertions(+), 32 deletions(-) diff --git a/frontend/src/scripts/db.ts b/frontend/src/scripts/db.ts index fdf0a88f1..7c320e866 100644 --- a/frontend/src/scripts/db.ts +++ b/frontend/src/scripts/db.ts @@ -183,27 +183,6 @@ export async function getUserResults(): Promise { } } -export async function getUserApeKeys(): Promise< - MonkeyTypes.ApeKeys | undefined -> { - const user = firebase.auth().currentUser; - if (user == null) return undefined; - if (dbSnapshot === null) return undefined; - if (dbSnapshot.apeKeys !== undefined) { - return dbSnapshot.apeKeys; - } else { - const response = await Ape.apeKeys.get(); - - if (response.status !== 200) { - Notifications.add("Error getting ape keys: " + response.message, -1); - return undefined; - } - - dbSnapshot.apeKeys = response.data as MonkeyTypes.ApeKeys; - return dbSnapshot.apeKeys; - } -} - export async function getUserHighestWpm( mode: M, mode2: MonkeyTypes.Mode2, diff --git a/frontend/src/scripts/popups/ape-keys-popup.ts b/frontend/src/scripts/popups/ape-keys-popup.ts index 255339168..33d420d24 100644 --- a/frontend/src/scripts/popups/ape-keys-popup.ts +++ b/frontend/src/scripts/popups/ape-keys-popup.ts @@ -1,10 +1,24 @@ -import * as DB from "../db"; import Ape from "../ape"; import * as Loader from "../elements/loader"; import * as Notifications from "../elements/notifications"; +let apeKeys: MonkeyTypes.ApeKeys = {}; + +async function getData(): Promise { + Loader.show(); + const response = await Ape.apeKeys.get(); + + if (response.status !== 200) { + Notifications.add("Error getting ape keys: " + response.message, -1); + return undefined; + } + + apeKeys = response.data as MonkeyTypes.ApeKeys; + Loader.hide(); +} + function refreshList(): void { - const data = DB.getSnapshot().apeKeys; + const data = apeKeys; if (!data) return; const table = $("#apeKeysPopupWrapper table tbody"); table.empty(); @@ -71,9 +85,7 @@ export function hide(): void { //show the popup export async function show(): Promise { if ($("#apeKeysPopupWrapper").hasClass("hidden")) { - Loader.show(); - await DB.getUserApeKeys(); - Loader.hide(); + await getData(); refreshList(); $("#apeKeysPopupWrapper") .stop(true, true) @@ -107,17 +119,15 @@ $(document).on("click", "#apeKeysPopup table .keyButtons .button", () => { $(document).on("click", "#apeKeysPopup table .icon-button", async (e) => { const keyId = $(e.target).closest("tr").attr("keyId") as string; - const snap = DB.getSnapshot(); - const key = snap.apeKeys?.[keyId]; - if (!key || !snap.apeKeys) return; + const key = apeKeys?.[keyId]; + if (!key || !apeKeys) return; Loader.show(); const response = await Ape.apeKeys.update(keyId, { enabled: !key.enabled }); Loader.hide(); if (response.status !== 200) { return Notifications.add("Failed to update key: " + response.message, -1); } - snap.apeKeys[keyId].enabled = !key.enabled; - DB.setSnapshot(snap); + apeKeys[keyId].enabled = !key.enabled; refreshList(); if (key.enabled) { Notifications.add("Key active", 1); diff --git a/frontend/src/scripts/types/types.d.ts b/frontend/src/scripts/types/types.d.ts index afd9c5898..d7a0c0036 100644 --- a/frontend/src/scripts/types/types.d.ts +++ b/frontend/src/scripts/types/types.d.ts @@ -415,7 +415,6 @@ declare namespace MonkeyTypes { quoteMod?: boolean; discordId?: string; config?: Config; - apeKeys?: ApeKeys; } type PartialRecord = { From 3521008928aa4a37799d512c325dd01b064ef2c0 Mon Sep 17 00:00:00 2001 From: Miodec Date: Sun, 6 Mar 2022 23:44:32 +0100 Subject: [PATCH 11/11] no need to modify local keys - they are redownloaded every time the popup is shown --- frontend/src/scripts/popups/simple-popups.ts | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/frontend/src/scripts/popups/simple-popups.ts b/frontend/src/scripts/popups/simple-popups.ts index 91e8f69da..10729e76f 100644 --- a/frontend/src/scripts/popups/simple-popups.ts +++ b/frontend/src/scripts/popups/simple-popups.ts @@ -783,11 +783,6 @@ list["generateApeKey"] = new SimplePopup( } else { const data = response.data; list["viewApeKey"].show([data.apeKey]); - const snap = DB.getSnapshot(); - if (snap.apeKeys) { - snap.apeKeys[data.apeKeyId] = data.apeKeyDetails; - DB.setSnapshot(snap); - } } }, () => { @@ -846,11 +841,6 @@ list["deleteApeKey"] = new SimplePopup( } Notifications.add("Key deleted", 1); - const snap = DB.getSnapshot(); - if (snap.apeKeys) { - delete snap.apeKeys[_thisPopup.parameters[0]]; - DB.setSnapshot(snap); - } ApeKeysPopup.show(); }, (_thisPopup) => { @@ -885,11 +875,6 @@ list["editApeKey"] = new SimplePopup( } Notifications.add("Key updated", 1); - const snap = DB.getSnapshot(); - if (snap.apeKeys) { - snap.apeKeys[_thisPopup.parameters[0]].name = input; - DB.setSnapshot(snap); - } ApeKeysPopup.show(); }, (_thisPopup) => {