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
This commit is contained in:
Jack 2022-03-06 16:34:48 +01:00 committed by GitHub
parent 134389515c
commit f221326f47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 557 additions and 20 deletions

View file

@ -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 });
}

View file

@ -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;
};

View file

@ -182,6 +182,28 @@ export async function getUserResults(): Promise<boolean> {
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<M extends MonkeyTypes.Mode>(
mode: M,
mode2: MonkeyTypes.Mode2<M>,

View file

@ -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);
}
});

View file

@ -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(
"<tr><td colspan='6' style='text-align: center;'>No keys found</td></tr>"
);
return;
}
apeKeyIds.forEach((apeKeyId) => {
const key = data[apeKeyId] as MonkeyTypes.ApeKey;
table.append(`
<tr keyId="${apeKeyId}">
<td>
<div class="icon-button">
${
key.enabled
? `<i class="fas fa-check-square"></i>`
: `<i class="far fa-fw fa-square"></i>`
}
</div>
</td>
<td>${key.name}</td>
<td>${moment(key.createdOn).format("DD MMM YYYY HH:mm")}</td>
<td>${moment(key.modifiedOn).format("DD MMM YYYY HH:mm")}</td>
<td>${
key.lastUsedOn === -1
? "-"
: moment(key.lastUsedOn).format("DD MMM YYYY HH:mm")
}</td>
<td>
<div class="keyButtons">
<div class="button edit">
<i class="fas fa-fw fa-pen"></i>
</div>
<div class="button delete">
<i class="fas fa-fw fa-trash-alt"></i>
</div>
</div>
</td>
</tr>
`);
});
}
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<void> {
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);
}
});

View file

@ -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<void>;
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<void>,
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 {
<input
type="number"
min="1"
val="${input.initVal}"
value="${input.initVal}"
placeholder="${input.placeholder}"
class="${input.hidden ? "hidden" : ""}"
${input.hidden ? "" : "required"}
@ -101,24 +116,38 @@ class SimplePopup {
} else if (this.type === "text") {
this.inputs.forEach((input) => {
if (input.type) {
el.find(".inputs").append(`
if (input.type === "textarea") {
el.find(".inputs").append(`
<textarea
placeholder="${input.placeholder}"
class="${input.hidden ? "hidden" : ""}"
${input.hidden ? "" : "required"}
${input.disabled ? "disabled" : ""}
autocomplete="off"
>${input.initVal}</textarea>
`);
} else {
el.find(".inputs").append(`
<input
type="${input.type}"
val="${input.initVal}"
placeholder="${input.placeholder}"
class="${input.hidden ? "hidden" : ""}"
${input.hidden ? "" : "required"}
autocomplete="off"
type="${input.type}"
value="${input.initVal}"
placeholder="${input.placeholder}"
class="${input.hidden ? "hidden" : ""}"
${input.hidden ? "" : "required"}
${input.disabled ? "disabled" : ""}
autocomplete="off"
>
`);
`);
}
} else {
el.find(".inputs").append(`
<input
type="text"
val="${input.initVal}"
value="${input.initVal}"
placeholder="${input.placeholder}"
class="${input.hidden ? "hidden" : ""}"
${input.hidden ? "" : "required"}
${input.disabled ? "disabled" : ""}
autocomplete="off"
>
`);
@ -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",

View file

@ -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<K extends keyof any, T> = {

View file

@ -10,6 +10,10 @@ textarea {
font-family: var(--font);
}
textarea {
resize: vertical;
}
input[type="range"] {
-webkit-appearance: none;
padding: 0;

View file

@ -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);

View file

@ -551,6 +551,30 @@
<div id="submitQuoteButton" class="button">Submit</div>
</div>
</div>
<div id="apeKeysPopupWrapper" class="hidden popupWrapper">
<div id="apeKeysPopup" mode="">
<div class="top">
<div class="title">Ape Keys</div>
<div class="button generateApeKey">
<i class="fas fa-plus"></i>
Generate new key
</div>
</div>
<table>
<thead>
<tr>
<td width="1px">active</td>
<td>name</td>
<td>created on</td>
<td>modified on</td>
<td>last used on</td>
<td width="1px"></td>
</tr>
</thead>
<tbody></tbody>
</table>
</div>
</div>
<div id="quoteReportPopupWrapper" class="popupWrapper hidden">
<div id="quoteReportPopup" mode="">
<div class="title">Report a Quote</div>
@ -4115,6 +4139,22 @@
</div>
</div>
</div>
<div class="section apeKeys needsAccount">
<h1>ape keys</h1>
<div class="text">
Generate Ape Keys to access certain API endpoints.
</div>
<div class="buttons">
<div
class="button"
id="showApeKeysPopup"
tabindex="0"
onclick="this.blur();"
>
open
</div>
</div>
</div>
<div class="section resetSettings">
<h1>reset settings</h1>
<div class="text">