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:
Bruce Berrios 2022-06-07 08:06:15 -04:00 committed by GitHub
parent 5ed1f166dd
commit f2998b1d28
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
18 changed files with 802 additions and 53 deletions

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

View 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");
}

View file

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

View file

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

View file

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

View 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;

View file

@ -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(

View file

@ -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.",
}),

View file

@ -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.",
}),

View file

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

View file

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

View file

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

View file

@ -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 = {

View file

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

View file

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

View file

@ -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",