diff --git a/backend/private/index.html b/backend/private/index.html
new file mode 100644
index 000000000..fd4f83131
--- /dev/null
+++ b/backend/private/index.html
@@ -0,0 +1,60 @@
+
+
+
+
+
+ API Server Configuration
+
+
+
+
+
+
+
+
+ Save Changes
+
+
+
diff --git a/backend/private/script.js b/backend/private/script.js
new file mode 100644
index 000000000..e2fa1d77e
--- /dev/null
+++ b/backend/private/script.js
@@ -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 = `This configuration is not yet supported`;
+
+ 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);
+ });
+};
diff --git a/backend/private/style.css b/backend/private/style.css
new file mode 100644
index 000000000..e28298aec
--- /dev/null
+++ b/backend/private/style.css
@@ -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);
+ }
+}
diff --git a/backend/src/api/controllers/configuration.ts b/backend/src/api/controllers/configuration.ts
new file mode 100644
index 000000000..df9900707
--- /dev/null
+++ b/backend/src/api/controllers/configuration.ts
@@ -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 {
+ const currentConfiguration = await Configuration.getLiveConfiguration();
+ return new MonkeyResponse("Configuration retrieved", currentConfiguration);
+}
+
+export async function getSchema(
+ _req: MonkeyTypes.Request
+): Promise {
+ return new MonkeyResponse(
+ "Configuration schema retrieved",
+ CONFIGURATION_FORM_SCHEMA
+ );
+}
+
+export async function updateConfiguration(
+ req: MonkeyTypes.Request
+): Promise {
+ const { configuration } = req.body;
+ const success = await Configuration.patchConfiguration(configuration);
+
+ if (!success) {
+ return new MonkeyResponse("Configuration update failed", {}, 500);
+ }
+
+ return new MonkeyResponse("Configuration updated");
+}
diff --git a/backend/src/api/controllers/quote.ts b/backend/src/api/controllers/quote.ts
index 2b92c0b05..c098c325b 100644
--- a/backend/src/api/controllers/quote.ts
+++ b/backend/src/api/controllers/quote.ts
@@ -131,8 +131,8 @@ export async function reportQuote(
): Promise {
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;
diff --git a/backend/src/api/controllers/result.ts b/backend/src/api/controllers/result.ts
index 24b71c4de..f1d083b3e 100644
--- a/backend/src/api/controllers/result.ts
+++ b/backend/src/api/controllers/result.ts
@@ -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
diff --git a/backend/src/api/controllers/user.ts b/backend/src/api/controllers/user.ts
index cec2e6a52..13c365b2d 100644
--- a/backend/src/api/controllers/user.ts
+++ b/backend/src/api/controllers/user.ts
@@ -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");
diff --git a/backend/src/api/routes/configuration.ts b/backend/src/api/routes/configuration.ts
new file mode 100644
index 000000000..7c6876296
--- /dev/null
+++ b/backend/src/api/routes/configuration.ts
@@ -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;
diff --git a/backend/src/api/routes/index.ts b/backend/src/api/routes/index.ts
index 32fdc0ec0..05a2e0f74 100644
--- a/backend/src/api/routes/index.ts
+++ b/backend/src/api/routes/index.ts
@@ -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(
diff --git a/backend/src/api/routes/quotes.ts b/backend/src/api/routes/quotes.ts
index 2dc764a2d..6f26f7e40 100644
--- a/backend/src/api/routes/quotes.ts
+++ b/backend/src/api/routes/quotes.ts
@@ -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.",
}),
diff --git a/backend/src/api/routes/results.ts b/backend/src/api/routes/results.ts
index 5da2a7a11..03f788e24 100644
--- a/backend/src/api/routes/results.ts
+++ b/backend/src/api/routes/results.ts
@@ -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.",
}),
diff --git a/backend/src/constants/base-configuration.ts b/backend/src/constants/base-configuration.ts
index 3aac39962..5a13df3a3 100644
--- a/backend/src/constants/base-configuration.ts
+++ b/backend/src/constants/base-configuration.ts
@@ -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,
+ },
+ },
+ },
+ },
+};
diff --git a/backend/src/init/configuration.ts b/backend/src/init/configuration.ts
index f8bb9d867..dc19ab8d5 100644
--- a/backend/src/init/configuration.ts
+++ b/backend/src/init/configuration.ts
@@ -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
): void {
if (
!_.isPlainObject(baseConfiguration) ||
@@ -109,3 +109,27 @@ async function pushConfiguration(
);
}
}
+
+export async function patchConfiguration(
+ configurationUpdates: Partial
+): Promise {
+ 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;
+}
diff --git a/backend/src/types/types.d.ts b/backend/src/types/types.d.ts
index 1d9d9601f..47a38d08e 100644
--- a/backend/src/types/types.d.ts
+++ b/backend/src/types/types.d.ts
@@ -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;
diff --git a/backend/src/utils/logger.ts b/backend/src/utils/logger.ts
index e24d8d7e7..7ff64ac4b 100644
--- a/backend/src/utils/logger.ts
+++ b/backend/src/utils/logger.ts
@@ -93,13 +93,17 @@ const logToDb = async (
const logsCollection = db.collection("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 = {
diff --git a/frontend/src/styles/core.scss b/frontend/src/styles/core.scss
index daf509d05..b7743e878 100644
--- a/frontend/src/styles/core.scss
+++ b/frontend/src/styles/core.scss
@@ -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;
+}
diff --git a/frontend/src/ts/ready.ts b/frontend/src/ts/ready.ts
index 4ddd287f8..54be61072 100644
--- a/frontend/src/ts/ready.ts
+++ b/frontend/src/ts/ready.ts
@@ -17,6 +17,9 @@ UpdateConfig.loadFromLocalStorage();
if (window.location.hostname === "localhost") {
$("#bottom .version .text").text("localhost");
$("#bottom .version").css("opacity", 1);
+ $("body").prepend(
+ `Configure Server`
+ );
} else {
Misc.getReleasesFromGitHub().then((v) => {
NewVersionNotification.show(v[0].name);
diff --git a/package.json b/package.json
index 766b9d627..9a6096d21 100644
--- a/package.json
+++ b/package.json
@@ -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",