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