mirror of
https://github.com/monkeytypegame/monkeytype.git
synced 2025-09-10 16:48:40 +08:00
Add server configuration panel (#3070) bruception
* Add server configuration panel * Remove unnecessaary check * Remove break * styling changes showing when configuration was saved * changing color based on response * Remove comment * Changes * Add support for arrays * Arbitrary nesting * Add array item controls * added button to quickly open the configuration panel * removed excessive padding * text inputs same height and style as checkboxes * monkey stylng Co-authored-by: Miodec <bartnikjack@gmail.com>
This commit is contained in:
parent
5ed1f166dd
commit
f2998b1d28
18 changed files with 802 additions and 53 deletions
60
backend/private/index.html
Normal file
60
backend/private/index.html
Normal file
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<meta name="viewport" content="width=device-width" />
|
||||
<title>API Server Configuration</title>
|
||||
<link href="style.css" rel="stylesheet" type="text/css" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div id="header">
|
||||
<div class="header-container">
|
||||
<div id="logo">
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
xmlns:xlink="http://www.w3.org/1999/xlink"
|
||||
style="isolation: isolate"
|
||||
viewBox="-680 -1030 300 180"
|
||||
>
|
||||
<g>
|
||||
<path
|
||||
d="M -430 -910 L -430 -910 C -424.481 -910 -420 -905.519 -420 -900 L -420 -900 C -420 -894.481 -424.481 -890 -430 -890 L -430 -890 C -435.519 -890 -440 -894.481 -440 -900 L -440 -900 C -440 -905.519 -435.519 -910 -430 -910 Z"
|
||||
></path>
|
||||
<path
|
||||
d=" M -570 -910 L -510 -910 C -504.481 -910 -500 -905.519 -500 -900 L -500 -900 C -500 -894.481 -504.481 -890 -510 -890 L -570 -890 C -575.519 -890 -580 -894.481 -580 -900 L -580 -900 C -580 -905.519 -575.519 -910 -570 -910 Z "
|
||||
></path>
|
||||
<path
|
||||
d="M -590 -970 L -590 -970 C -584.481 -970 -580 -965.519 -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 C -600 -965.519 -595.519 -970 -590 -970 Z"
|
||||
></path>
|
||||
<path
|
||||
d=" M -639.991 -960.515 C -639.72 -976.836 -626.385 -990 -610 -990 L -610 -990 C -602.32 -990 -595.31 -987.108 -590 -982.355 C -584.69 -987.108 -577.68 -990 -570 -990 L -570 -990 C -553.615 -990 -540.28 -976.836 -540.009 -960.515 C -540.001 -960.345 -540 -960.172 -540 -960 L -540 -960 L -540 -940 C -540 -934.481 -544.481 -930 -550 -930 L -550 -930 C -555.519 -930 -560 -934.481 -560 -940 L -560 -960 L -560 -960 C -560 -965.519 -564.481 -970 -570 -970 C -575.519 -970 -580 -965.519 -580 -960 L -580 -960 L -580 -960 L -580 -940 C -580 -934.481 -584.481 -930 -590 -930 L -590 -930 C -595.519 -930 -600 -934.481 -600 -940 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 L -600 -960 C -600 -965.519 -604.481 -970 -610 -970 C -615.519 -970 -620 -965.519 -620 -960 L -620 -960 L -620 -940 C -620 -934.481 -624.481 -930 -630 -930 L -630 -930 C -635.519 -930 -640 -934.481 -640 -940 L -640 -960 L -640 -960 C -640 -960.172 -639.996 -960.344 -639.991 -960.515 Z "
|
||||
></path>
|
||||
<path
|
||||
d=" M -460 -930 L -460 -900 C -460 -894.481 -464.481 -890 -470 -890 L -470 -890 C -475.519 -890 -480 -894.481 -480 -900 L -480 -930 L -508.82 -930 C -514.99 -930 -520 -934.481 -520 -940 L -520 -940 C -520 -945.519 -514.99 -950 -508.82 -950 L -431.18 -950 C -425.01 -950 -420 -945.519 -420 -940 L -420 -940 C -420 -934.481 -425.01 -930 -431.18 -930 L -460 -930 Z "
|
||||
></path>
|
||||
<path
|
||||
d="M -470 -990 L -430 -990 C -424.481 -990 -420 -985.519 -420 -980 L -420 -980 C -420 -974.481 -424.481 -970 -430 -970 L -470 -970 C -475.519 -970 -480 -974.481 -480 -980 L -480 -980 C -480 -985.519 -475.519 -990 -470 -990 Z"
|
||||
></path>
|
||||
<path
|
||||
d=" M -630 -910 L -610 -910 C -604.481 -910 -600 -905.519 -600 -900 L -600 -900 C -600 -894.481 -604.481 -890 -610 -890 L -630 -890 C -635.519 -890 -640 -894.481 -640 -900 L -640 -900 C -640 -905.519 -635.519 -910 -630 -910 Z "
|
||||
></path>
|
||||
<path
|
||||
d=" M -515 -990 L -510 -990 C -504.481 -990 -500 -985.519 -500 -980 L -500 -980 C -500 -974.481 -504.481 -970 -510 -970 L -515 -970 C -520.519 -970 -525 -974.481 -525 -980 L -525 -980 C -525 -985.519 -520.519 -990 -515 -990 Z "
|
||||
></path>
|
||||
<path
|
||||
d=" M -660 -910 L -680 -910 L -680 -980 C -680 -1007.596 -657.596 -1030 -630 -1030 L -430 -1030 C -402.404 -1030 -380 -1007.596 -380 -980 L -380 -900 C -380 -872.404 -402.404 -850 -430 -850 L -630 -850 C -657.596 -850 -680 -872.404 -680 -900 L -680 -920 L -660 -920 L -660 -900 C -660 -883.443 -646.557 -870 -630 -870 L -430 -870 C -413.443 -870 -400 -883.443 -400 -900 L -400 -980 C -400 -996.557 -413.443 -1010 -430 -1010 L -630 -1010 C -646.557 -1010 -660 -996.557 -660 -980 L -660 -910 Z "
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<h1>API Server Configuration</h1>
|
||||
</div>
|
||||
</div>
|
||||
<div id="root">
|
||||
<span id="form-loader" class="loader"></span>
|
||||
</div>
|
||||
<div id="save">Save Changes</div>
|
||||
<script src="script.js"></script>
|
||||
</body>
|
||||
</html>
|
251
backend/private/script.js
Normal file
251
backend/private/script.js
Normal file
|
@ -0,0 +1,251 @@
|
|||
let state = {};
|
||||
let schema = {};
|
||||
|
||||
const buildLabel = (elementType, text) => {
|
||||
const labelElement = document.createElement("label");
|
||||
labelElement.innerHTML = text;
|
||||
labelElement.style.fontWeight = elementType === "group" ? "bold" : "lighter";
|
||||
|
||||
return labelElement;
|
||||
};
|
||||
|
||||
const buildNumberInput = (schema, parentState, key) => {
|
||||
const input = document.createElement("input");
|
||||
input.classList.add("base-input");
|
||||
input.type = "number";
|
||||
input.value = parentState[key];
|
||||
input.min = schema.min || 0;
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
const normalizedValue = parseFloat(input.value, 10);
|
||||
parentState[key] = normalizedValue;
|
||||
});
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
const buildBooleanInput = (parentState, key) => {
|
||||
const input = document.createElement("input");
|
||||
input.classList.add("base-input");
|
||||
input.type = "checkbox";
|
||||
input.checked = parentState[key] || false;
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
parentState[key] = input.checked;
|
||||
});
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
const buildStringInput = (parentState, key) => {
|
||||
const input = document.createElement("input");
|
||||
input.classList.add("base-input");
|
||||
input.type = "text";
|
||||
input.value = parentState[key] || "";
|
||||
|
||||
input.addEventListener("change", () => {
|
||||
parentState[key] = input.value;
|
||||
});
|
||||
|
||||
return input;
|
||||
};
|
||||
|
||||
const defaultValueForType = (type) => {
|
||||
switch (type) {
|
||||
case "number":
|
||||
return 0;
|
||||
case "boolean":
|
||||
return false;
|
||||
case "string":
|
||||
return "";
|
||||
case "array":
|
||||
return [];
|
||||
case "object":
|
||||
return {};
|
||||
}
|
||||
|
||||
return null;
|
||||
};
|
||||
|
||||
const arrayFormElementDecorator = (childElement, parentState, index) => {
|
||||
const decoratedElement = document.createElement("div");
|
||||
decoratedElement.classList.add("array-form-element-decorator");
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.innerHTML = "X";
|
||||
removeButton.classList.add("array-input", "array-input-delete", "button");
|
||||
removeButton.addEventListener("click", () => {
|
||||
parentState.splice(index, 1);
|
||||
rerender();
|
||||
});
|
||||
|
||||
decoratedElement.appendChild(childElement);
|
||||
decoratedElement.appendChild(removeButton);
|
||||
|
||||
return decoratedElement;
|
||||
};
|
||||
|
||||
const buildArrayInput = (schema, parentState) => {
|
||||
const itemType = schema.items.type;
|
||||
const inputControlsDiv = document.createElement("div");
|
||||
inputControlsDiv.classList.add("array-input-controls");
|
||||
|
||||
const addButton = document.createElement("button");
|
||||
addButton.innerHTML = "Add One";
|
||||
addButton.classList.add("array-input", "button");
|
||||
addButton.addEventListener("click", () => {
|
||||
parentState.push(defaultValueForType(itemType));
|
||||
rerender();
|
||||
});
|
||||
|
||||
const removeButton = document.createElement("button");
|
||||
removeButton.innerHTML = "Delete All";
|
||||
removeButton.classList.add("array-input", "array-input-delete", "button");
|
||||
removeButton.addEventListener("click", () => {
|
||||
parentState.splice(0, parentState.length);
|
||||
rerender();
|
||||
});
|
||||
|
||||
inputControlsDiv.appendChild(addButton);
|
||||
inputControlsDiv.appendChild(removeButton);
|
||||
|
||||
return inputControlsDiv;
|
||||
};
|
||||
|
||||
const buildUnknownInput = () => {
|
||||
const disclaimer = document.createElement("div");
|
||||
disclaimer.innerHTML = `<i class="unknown-input">This configuration is not yet supported</i>`;
|
||||
|
||||
return disclaimer;
|
||||
};
|
||||
|
||||
const render = (state, schema) => {
|
||||
const build = (
|
||||
schema,
|
||||
state,
|
||||
parentState,
|
||||
currentKey = "",
|
||||
path = "configuration"
|
||||
) => {
|
||||
const parent = document.createElement("div");
|
||||
parent.classList.add("form-element");
|
||||
|
||||
const { type, label, fields, items } = schema;
|
||||
|
||||
if (label) {
|
||||
parent.appendChild(buildLabel(type, label));
|
||||
}
|
||||
|
||||
parent.id = path;
|
||||
|
||||
if (type === "object") {
|
||||
const entries = Object.entries(fields);
|
||||
entries.forEach(([key, value]) => {
|
||||
if (!state[key]) {
|
||||
state[key] = defaultValueForType(value.type);
|
||||
}
|
||||
|
||||
const childElement = build(
|
||||
value,
|
||||
state[key],
|
||||
state,
|
||||
key,
|
||||
`${path}.${key}`
|
||||
);
|
||||
parent.appendChild(childElement);
|
||||
});
|
||||
} else if (type === "array") {
|
||||
const arrayInputControls = buildArrayInput(schema, state);
|
||||
parent.appendChild(arrayInputControls);
|
||||
|
||||
if (state && state.length > 0) {
|
||||
state.forEach((element, index) => {
|
||||
const childElement = build(
|
||||
items,
|
||||
element,
|
||||
state,
|
||||
`${currentKey}[${index}]`,
|
||||
`${path}[${index}]`
|
||||
);
|
||||
|
||||
const decoratedChildElement = arrayFormElementDecorator(
|
||||
childElement,
|
||||
state,
|
||||
index
|
||||
);
|
||||
parent.appendChild(decoratedChildElement);
|
||||
});
|
||||
}
|
||||
} else if (type === "number") {
|
||||
parent.appendChild(buildNumberInput(schema, parentState, currentKey));
|
||||
parent.classList.add("input-label");
|
||||
} else if (type === "string") {
|
||||
parent.appendChild(buildStringInput(parentState, currentKey));
|
||||
parent.classList.add("input-label");
|
||||
} else if (type === "boolean") {
|
||||
parent.appendChild(buildBooleanInput(parentState, currentKey));
|
||||
parent.classList.add("input-label");
|
||||
} else {
|
||||
parent.appendChild(buildUnknownInput());
|
||||
}
|
||||
|
||||
return parent;
|
||||
};
|
||||
|
||||
return build(schema, state, state);
|
||||
};
|
||||
|
||||
function rerender() {
|
||||
const root = document.querySelector("#root");
|
||||
root.innerHTML = "";
|
||||
root?.append(render(state, schema));
|
||||
}
|
||||
|
||||
window.onload = async () => {
|
||||
const schemaResponse = await fetch("/configuration/schema");
|
||||
const dataResponse = await fetch("/configuration");
|
||||
|
||||
const schemaResponseJson = await schemaResponse.json();
|
||||
const dataResponseJson = await dataResponse.json();
|
||||
|
||||
const { data: formSchema } = schemaResponseJson;
|
||||
const { data: initialData } = dataResponseJson;
|
||||
|
||||
state = initialData;
|
||||
schema = formSchema;
|
||||
|
||||
rerender();
|
||||
|
||||
const saveButton = document.querySelector("#save");
|
||||
|
||||
saveButton?.addEventListener("click", async () => {
|
||||
if (saveButton.disabled) {
|
||||
return;
|
||||
}
|
||||
|
||||
saveButton.innerHTML = "Saving...";
|
||||
saveButton.disabled = true;
|
||||
const response = await fetch("/configuration", {
|
||||
method: "PATCH",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
configuration: state,
|
||||
}),
|
||||
});
|
||||
if (response.status === 200) {
|
||||
saveButton.innerHTML = "Saved!";
|
||||
saveButton.classList.add("good");
|
||||
} else {
|
||||
saveButton.innerHTML = "Failed!";
|
||||
saveButton.classList.add("bad");
|
||||
}
|
||||
setTimeout(() => {
|
||||
saveButton.innerHTML = "Save Changes";
|
||||
saveButton.classList.remove("good");
|
||||
saveButton.classList.remove("bad");
|
||||
saveButton.disabled = false;
|
||||
}, 3000);
|
||||
});
|
||||
};
|
187
backend/private/style.css
Normal file
187
backend/private/style.css
Normal file
|
@ -0,0 +1,187 @@
|
|||
@import url("https://fonts.googleapis.com/css2?family=Roboto+Mono&display=swap");
|
||||
|
||||
:root {
|
||||
--roundness: 0.5rem;
|
||||
/* --sub-color: #e1e1e1; */
|
||||
/* --highlight-color: #0085a8; */
|
||||
--bg-color: #323437;
|
||||
--main-color: #e2b714;
|
||||
--caret-color: #e2b714;
|
||||
--sub-color: #646669;
|
||||
--sub-alt-color: #2c2e31;
|
||||
--text-color: #d1d0c5;
|
||||
--error-color: #ca4754;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: "Roboto Mono", sans-serif;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
background: var(--bg-color);
|
||||
}
|
||||
|
||||
#header {
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.header-container {
|
||||
padding: 1rem 0;
|
||||
max-width: 60rem;
|
||||
margin: 0 auto;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
#logo {
|
||||
align-items: center;
|
||||
background-color: transparent;
|
||||
display: grid;
|
||||
width: 3rem;
|
||||
margin-right: 1rem;
|
||||
}
|
||||
|
||||
#header h1 {
|
||||
font-size: 1.5rem;
|
||||
}
|
||||
|
||||
#logo path {
|
||||
fill: var(--main-color);
|
||||
}
|
||||
|
||||
#root {
|
||||
padding: 2rem;
|
||||
background-color: #fff;
|
||||
max-width: 60rem;
|
||||
margin: 0rem auto;
|
||||
border-radius: var(--roundness);
|
||||
}
|
||||
|
||||
.button {
|
||||
background-color: var(--bg-color);
|
||||
color: #fff;
|
||||
padding: 0.5rem 1rem;
|
||||
border-radius: 0.25rem;
|
||||
border: none;
|
||||
cursor: pointer;
|
||||
font-size: 1rem;
|
||||
margin-right: 1rem;
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
|
||||
.array-input {
|
||||
margin: 1rem auto;
|
||||
}
|
||||
|
||||
.array-input-delete {
|
||||
margin-left: 1rem;
|
||||
background-color: #d84b4b;
|
||||
}
|
||||
|
||||
#save {
|
||||
position: fixed;
|
||||
right: 6rem;
|
||||
bottom: 3rem;
|
||||
background-color: var(--sub-alt-color);
|
||||
color: var(--text-color);
|
||||
font-style: bold;
|
||||
border-radius: 3px;
|
||||
padding: 1rem 2rem;
|
||||
cursor: pointer;
|
||||
display: inline-block;
|
||||
width: 125px;
|
||||
text-align: center;
|
||||
transition: 0.125s;
|
||||
}
|
||||
|
||||
#save:hover {
|
||||
background-color: var(--text-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
#save.good {
|
||||
background-color: var(--main-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
#save.bad {
|
||||
background-color: var(--error-color);
|
||||
color: var(--bg-color);
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.base-input {
|
||||
margin: 0.5rem;
|
||||
border: 1px solid #767676;
|
||||
border-radius: calc(var(--roundness) / 2);
|
||||
background-color: #fff;
|
||||
transition: all 0.2s ease-in-out;
|
||||
font-size: 1rem;
|
||||
padding: 0.25rem;
|
||||
font-family: "Roboto Mono";
|
||||
}
|
||||
|
||||
input[type="checkbox"] {
|
||||
display: inline-block;
|
||||
accent-color: var(--main-color);
|
||||
color: white;
|
||||
width: 1.5rem;
|
||||
height: 1.5rem;
|
||||
}
|
||||
|
||||
.form-element {
|
||||
padding-left: 3rem;
|
||||
padding-top: 0.25rem;
|
||||
padding-bottom: 0.25rem;
|
||||
border-left: var(--sub-color) 0.5px solid;
|
||||
/* border-bottom: var(--sub-color) 0.5px dotted; */
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.array-form-element-decorator {
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
#root > .form-element:first-child {
|
||||
border-left: none;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.shadow {
|
||||
box-shadow: 0 0.5rem 1rem rgba(0, 0, 0, 0.15);
|
||||
}
|
||||
|
||||
.input-label {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.unknown-input {
|
||||
color: #d84b4b;
|
||||
}
|
||||
|
||||
.loader {
|
||||
margin: auto;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border: 5px solid var(--bg-color);
|
||||
border-bottom-color: transparent;
|
||||
border-radius: 50%;
|
||||
display: block;
|
||||
box-sizing: border-box;
|
||||
animation: rotation 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes rotation {
|
||||
0% {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
32
backend/src/api/controllers/configuration.ts
Normal file
32
backend/src/api/controllers/configuration.ts
Normal file
|
@ -0,0 +1,32 @@
|
|||
import * as Configuration from "../../init/configuration";
|
||||
import { MonkeyResponse } from "../../utils/monkey-response";
|
||||
import { CONFIGURATION_FORM_SCHEMA } from "../../constants/base-configuration";
|
||||
|
||||
export async function getConfiguration(
|
||||
_req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const currentConfiguration = await Configuration.getLiveConfiguration();
|
||||
return new MonkeyResponse("Configuration retrieved", currentConfiguration);
|
||||
}
|
||||
|
||||
export async function getSchema(
|
||||
_req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
return new MonkeyResponse(
|
||||
"Configuration schema retrieved",
|
||||
CONFIGURATION_FORM_SCHEMA
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateConfiguration(
|
||||
req: MonkeyTypes.Request
|
||||
): Promise<MonkeyResponse> {
|
||||
const { configuration } = req.body;
|
||||
const success = await Configuration.patchConfiguration(configuration);
|
||||
|
||||
if (!success) {
|
||||
return new MonkeyResponse("Configuration update failed", {}, 500);
|
||||
}
|
||||
|
||||
return new MonkeyResponse("Configuration updated");
|
||||
}
|
|
@ -131,8 +131,8 @@ export async function reportQuote(
|
|||
): Promise<MonkeyResponse> {
|
||||
const { uid } = req.ctx.decodedToken;
|
||||
const {
|
||||
quoteReport: { maxReports, contentReportLimit },
|
||||
} = req.ctx.configuration;
|
||||
reporting: { maxReports, contentReportLimit },
|
||||
} = req.ctx.configuration.quotes;
|
||||
|
||||
const { quoteId, quoteLanguage, reason, comment, captcha } = req.body;
|
||||
|
||||
|
|
|
@ -112,7 +112,7 @@ export async function addResult(
|
|||
delete result.hash;
|
||||
delete result.stringified;
|
||||
if (
|
||||
req.ctx.configuration.resultObjectHashCheck.enabled &&
|
||||
req.ctx.configuration.results.objectHashCheckEnabled &&
|
||||
resulthash.length === 40
|
||||
) {
|
||||
//if its not 64 that means client is still using old hashing package
|
||||
|
|
|
@ -330,7 +330,7 @@ export async function addFavoriteQuote(
|
|||
uid,
|
||||
language,
|
||||
quoteId,
|
||||
req.ctx.configuration.favoriteQuotes.maxFavorites
|
||||
req.ctx.configuration.quotes.maxFavorites
|
||||
);
|
||||
|
||||
return new MonkeyResponse("Quote added to favorites");
|
||||
|
|
22
backend/src/api/routes/configuration.ts
Normal file
22
backend/src/api/routes/configuration.ts
Normal file
|
@ -0,0 +1,22 @@
|
|||
import joi from "joi";
|
||||
import { Router } from "express";
|
||||
import { asyncHandler, validateRequest } from "../../middlewares/api-utils";
|
||||
import * as ConfigurationController from "../controllers/configuration";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.get("/", asyncHandler(ConfigurationController.getConfiguration));
|
||||
|
||||
router.patch(
|
||||
"/",
|
||||
validateRequest({
|
||||
body: {
|
||||
configuration: joi.object(),
|
||||
},
|
||||
}),
|
||||
asyncHandler(ConfigurationController.updateConfiguration)
|
||||
);
|
||||
|
||||
router.get("/schema", asyncHandler(ConfigurationController.getSchema));
|
||||
|
||||
export default router;
|
|
@ -1,11 +1,14 @@
|
|||
import _ from "lodash";
|
||||
import psas from "./psas";
|
||||
import users from "./users";
|
||||
import { join } from "path";
|
||||
import express from "express";
|
||||
import quotes from "./quotes";
|
||||
import configs from "./configs";
|
||||
import results from "./results";
|
||||
import presets from "./presets";
|
||||
import apeKeys from "./ape-keys";
|
||||
import configuration from "./configuration";
|
||||
import { version } from "../../version";
|
||||
import leaderboards from "./leaderboards";
|
||||
import addSwaggerMiddlewares from "./swagger";
|
||||
|
@ -34,6 +37,11 @@ function addApiRoutes(app: Application): void {
|
|||
res.sendStatus(404);
|
||||
});
|
||||
|
||||
if (process.env.MODE === "dev") {
|
||||
app.use("/configure", express.static(join(__dirname, "../../../private")));
|
||||
app.use("/configuration", configuration);
|
||||
}
|
||||
|
||||
addSwaggerMiddlewares(app);
|
||||
|
||||
app.use(
|
||||
|
|
|
@ -30,7 +30,7 @@ router.post(
|
|||
"/",
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.quoteSubmit.enabled;
|
||||
return configuration.quotes.submissionsEnabled;
|
||||
},
|
||||
invalidMessage:
|
||||
"Quote submission is disabled temporarily. The queue is quite long and we need some time to catch up.",
|
||||
|
@ -109,7 +109,7 @@ router.post(
|
|||
"/report",
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.quoteReport.enabled;
|
||||
return configuration.quotes.reporting.enabled;
|
||||
},
|
||||
invalidMessage: "Quote reporting is unavailable.",
|
||||
}),
|
||||
|
|
|
@ -24,7 +24,7 @@ router.post(
|
|||
"/",
|
||||
validateConfiguration({
|
||||
criteria: (configuration) => {
|
||||
return configuration.enableSavingResults.enabled;
|
||||
return configuration.results.savingEnabled;
|
||||
},
|
||||
invalidMessage: "Results are not being saved at this time.",
|
||||
}),
|
||||
|
|
|
@ -3,18 +3,20 @@
|
|||
* To add a new configuration. Simply add it to this object.
|
||||
* When changing this template, please follow the principle of "Secure by default" (https://en.wikipedia.org/wiki/Secure_by_default).
|
||||
*/
|
||||
const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
||||
export const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
||||
maintenance: false,
|
||||
quoteReport: {
|
||||
enabled: false,
|
||||
maxReports: 0,
|
||||
contentReportLimit: 0,
|
||||
results: {
|
||||
savingEnabled: false,
|
||||
objectHashCheckEnabled: false,
|
||||
},
|
||||
quoteSubmit: {
|
||||
enabled: false,
|
||||
},
|
||||
resultObjectHashCheck: {
|
||||
enabled: false,
|
||||
quotes: {
|
||||
reporting: {
|
||||
enabled: false,
|
||||
maxReports: 0,
|
||||
contentReportLimit: 0,
|
||||
},
|
||||
submissionsEnabled: false,
|
||||
maxFavorites: 0,
|
||||
},
|
||||
apeKeys: {
|
||||
endpointsEnabled: false,
|
||||
|
@ -23,12 +25,6 @@ const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
apeKeyBytes: 24,
|
||||
apeKeySaltRounds: 5,
|
||||
},
|
||||
enableSavingResults: {
|
||||
enabled: false,
|
||||
},
|
||||
favoriteQuotes: {
|
||||
maxFavorites: 0,
|
||||
},
|
||||
autoBan: {
|
||||
enabled: false,
|
||||
maxCount: 5,
|
||||
|
@ -45,4 +41,160 @@ const BASE_CONFIGURATION: MonkeyTypes.Configuration = {
|
|||
},
|
||||
};
|
||||
|
||||
export default BASE_CONFIGURATION;
|
||||
export const CONFIGURATION_FORM_SCHEMA = {
|
||||
type: "object",
|
||||
label: "Server Configuration",
|
||||
fields: {
|
||||
maintenance: {
|
||||
type: "boolean",
|
||||
label: "In Maintenance",
|
||||
},
|
||||
results: {
|
||||
type: "object",
|
||||
label: "Results",
|
||||
fields: {
|
||||
savingEnabled: {
|
||||
type: "boolean",
|
||||
label: "Saving Results",
|
||||
},
|
||||
objectHashCheckEnabled: {
|
||||
type: "boolean",
|
||||
label: "Object Hash Check",
|
||||
},
|
||||
},
|
||||
},
|
||||
quotes: {
|
||||
type: "object",
|
||||
label: "Quotes",
|
||||
fields: {
|
||||
reporting: {
|
||||
type: "object",
|
||||
label: "Reporting",
|
||||
fields: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
},
|
||||
maxReports: {
|
||||
type: "number",
|
||||
label: "Max Reports",
|
||||
},
|
||||
contentReportLimit: {
|
||||
type: "number",
|
||||
label: "Content Report Limit",
|
||||
},
|
||||
},
|
||||
},
|
||||
submissionsEnabled: {
|
||||
type: "boolean",
|
||||
label: "Submissions Enabled",
|
||||
},
|
||||
maxFavorites: {
|
||||
type: "number",
|
||||
label: "Max Favorites",
|
||||
},
|
||||
},
|
||||
},
|
||||
apeKeys: {
|
||||
type: "object",
|
||||
label: "Ape Keys",
|
||||
fields: {
|
||||
endpointsEnabled: {
|
||||
type: "boolean",
|
||||
label: "Endpoints Enabled",
|
||||
},
|
||||
acceptKeys: {
|
||||
type: "boolean",
|
||||
label: "Accept Keys",
|
||||
},
|
||||
maxKeysPerUser: {
|
||||
type: "number",
|
||||
label: "Max Keys Per User",
|
||||
min: 0,
|
||||
},
|
||||
apeKeyBytes: {
|
||||
type: "number",
|
||||
label: "Ape Key Bytes",
|
||||
min: 24,
|
||||
},
|
||||
apeKeySaltRounds: {
|
||||
type: "number",
|
||||
label: "Ape Key Salt Rounds",
|
||||
min: 5,
|
||||
},
|
||||
},
|
||||
},
|
||||
autoBan: {
|
||||
type: "object",
|
||||
label: "Auto Ban",
|
||||
fields: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
},
|
||||
maxCount: {
|
||||
type: "number",
|
||||
label: "Max Count",
|
||||
min: 0,
|
||||
},
|
||||
maxHours: {
|
||||
type: "number",
|
||||
label: "Max Hours",
|
||||
min: 0,
|
||||
},
|
||||
},
|
||||
},
|
||||
dailyLeaderboards: {
|
||||
type: "object",
|
||||
label: "Daily Leaderboards",
|
||||
fields: {
|
||||
enabled: {
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
},
|
||||
maxResults: {
|
||||
type: "number",
|
||||
label: "Max Results",
|
||||
min: 0,
|
||||
},
|
||||
leaderboardExpirationTimeInDays: {
|
||||
type: "number",
|
||||
label: "Leaderboard Expiration Time In Days",
|
||||
min: 0,
|
||||
},
|
||||
validModeRules: {
|
||||
type: "array",
|
||||
label: "Valid Mode Rules",
|
||||
items: {
|
||||
type: "object",
|
||||
label: "Rule",
|
||||
fields: {
|
||||
language: {
|
||||
type: "string",
|
||||
label: "Language",
|
||||
},
|
||||
mode: {
|
||||
type: "string",
|
||||
label: "Mode",
|
||||
},
|
||||
mode2: {
|
||||
type: "string",
|
||||
label: "Secondary Mode",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
dailyLeaderboardCacheSize: {
|
||||
type: "number",
|
||||
label: "Daily Leaderboard Cache Size",
|
||||
min: 1,
|
||||
},
|
||||
topResultsToAnnounce: {
|
||||
type: "number",
|
||||
label: "Top Results To Announce",
|
||||
min: 1,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
import * as db from "./db";
|
||||
import _ from "lodash";
|
||||
import * as db from "./db";
|
||||
import { ObjectId } from "mongodb";
|
||||
import Logger from "../utils/logger";
|
||||
import { identity } from "../utils/misc";
|
||||
import BASE_CONFIGURATION from "../constants/base-configuration";
|
||||
import { ObjectId } from "mongodb";
|
||||
import { BASE_CONFIGURATION } from "../constants/base-configuration";
|
||||
|
||||
const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes
|
||||
|
||||
function mergeConfigurations(
|
||||
baseConfiguration: MonkeyTypes.Configuration,
|
||||
liveConfiguration: MonkeyTypes.Configuration
|
||||
liveConfiguration: Partial<MonkeyTypes.Configuration>
|
||||
): void {
|
||||
if (
|
||||
!_.isPlainObject(baseConfiguration) ||
|
||||
|
@ -109,3 +109,27 @@ async function pushConfiguration(
|
|||
);
|
||||
}
|
||||
}
|
||||
|
||||
export async function patchConfiguration(
|
||||
configurationUpdates: Partial<MonkeyTypes.Configuration>
|
||||
): Promise<boolean> {
|
||||
try {
|
||||
const currentConfiguration = _.cloneDeep(configuration);
|
||||
mergeConfigurations(currentConfiguration, configurationUpdates);
|
||||
|
||||
await db
|
||||
.collection("configuration")
|
||||
.updateOne({}, { $set: currentConfiguration }, { upsert: true });
|
||||
|
||||
await getLiveConfiguration();
|
||||
} catch (error) {
|
||||
Logger.logToDb(
|
||||
"patch_configuration_failure",
|
||||
`Could not patch configuration: ${error.message}`
|
||||
);
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
26
backend/src/types/types.d.ts
vendored
26
backend/src/types/types.d.ts
vendored
|
@ -11,16 +11,18 @@ declare namespace MonkeyTypes {
|
|||
|
||||
interface Configuration {
|
||||
maintenance: boolean;
|
||||
quoteReport: {
|
||||
enabled: boolean;
|
||||
maxReports: number;
|
||||
contentReportLimit: number;
|
||||
quotes: {
|
||||
reporting: {
|
||||
enabled: boolean;
|
||||
maxReports: number;
|
||||
contentReportLimit: number;
|
||||
};
|
||||
submissionsEnabled: boolean;
|
||||
maxFavorites: number;
|
||||
};
|
||||
quoteSubmit: {
|
||||
enabled: boolean;
|
||||
};
|
||||
resultObjectHashCheck: {
|
||||
enabled: boolean;
|
||||
results: {
|
||||
savingEnabled: boolean;
|
||||
objectHashCheckEnabled: boolean;
|
||||
};
|
||||
apeKeys: {
|
||||
endpointsEnabled: boolean;
|
||||
|
@ -29,12 +31,6 @@ declare namespace MonkeyTypes {
|
|||
apeKeyBytes: number;
|
||||
apeKeySaltRounds: number;
|
||||
};
|
||||
enableSavingResults: {
|
||||
enabled: boolean;
|
||||
};
|
||||
favoriteQuotes: {
|
||||
maxFavorites: number;
|
||||
};
|
||||
autoBan: {
|
||||
enabled: boolean;
|
||||
maxCount: number;
|
||||
|
|
|
@ -93,13 +93,17 @@ const logToDb = async (
|
|||
const logsCollection = db.collection<Log>("logs");
|
||||
|
||||
logger.info(`${event}\t${uid}\t${JSON.stringify(message)}`);
|
||||
logsCollection.insertOne({
|
||||
_id: new ObjectId(),
|
||||
timestamp: Date.now(),
|
||||
uid: uid ?? "",
|
||||
event,
|
||||
message,
|
||||
});
|
||||
logsCollection
|
||||
.insertOne({
|
||||
_id: new ObjectId(),
|
||||
timestamp: Date.now(),
|
||||
uid: uid ?? "",
|
||||
event,
|
||||
message,
|
||||
})
|
||||
.catch((error) => {
|
||||
logger.error(`Could not log to db: ${error.message}`);
|
||||
});
|
||||
};
|
||||
|
||||
const Logger = {
|
||||
|
|
|
@ -434,3 +434,13 @@ key {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
.configureAPI.button {
|
||||
position: fixed;
|
||||
left: 2rem;
|
||||
bottom: 2rem;
|
||||
display: grid;
|
||||
grid-auto-flow: column;
|
||||
gap: 0.5rem;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
|
|
@ -17,6 +17,9 @@ UpdateConfig.loadFromLocalStorage();
|
|||
if (window.location.hostname === "localhost") {
|
||||
$("#bottom .version .text").text("localhost");
|
||||
$("#bottom .version").css("opacity", 1);
|
||||
$("body").prepend(
|
||||
`<a class='button configureAPI' href='http://localhost:5005/configure/' target='_blank'><i class="fas fa-fw fa-server"></i>Configure Server</a>`
|
||||
);
|
||||
} else {
|
||||
Misc.getReleasesFromGitHub().then((v) => {
|
||||
NewVersionNotification.show(v[0].name);
|
||||
|
|
|
@ -16,9 +16,9 @@
|
|||
"audit-fe": "cd frontend && npm run audit",
|
||||
"deploy-live": "cd frontend && npm run deploy-live",
|
||||
"build-fe": "cd ./frontend && npm run build-live",
|
||||
"pretty": "prettier --check './backend/**/*.{ts,json}' './frontend/**/*.{ts,js,scss}' './frontend/static/**/*.{json,html}'",
|
||||
"pretty-code": "prettier --check './backend/**/*.{ts,js,json}' './frontend/**/*.{ts,js}' './frontend/src/**/*.scss'",
|
||||
"pretty-fix": "prettier --write './backend/**/*.{ts,json}' './frontend/**/*.{ts,js,scss}' './frontend/static/**/*.{json,html}'",
|
||||
"pretty": "prettier --check './backend/**/*.{ts,json,js,css,html}' './frontend/**/*.{ts,js,scss}' './frontend/static/**/*.{json,html}'",
|
||||
"pretty-code": "prettier --check './backend/**/*.{ts,js,json,css,html}' './frontend/**/*.{ts,js}' './frontend/src/**/*.scss'",
|
||||
"pretty-fix": "prettier --write './backend/**/*.{ts,json,js,css,html}' './frontend/**/*.{ts,js,scss}' './frontend/static/**/*.{json,html}'",
|
||||
"pr-check-lint-json": "cd frontend && npx gulp pr-check-lint-json",
|
||||
"pr-check-quote-json": "cd frontend && npx gulp pr-check-quote-json",
|
||||
"pr-check-language-json": "cd frontend && npx gulp pr-check-language-json",
|
||||
|
|
Loading…
Add table
Reference in a new issue