diff --git a/backend/api/routes/quotes.js b/backend/api/routes/quotes.js index 38fc18d05..9180f7168 100644 --- a/backend/api/routes/quotes.js +++ b/backend/api/routes/quotes.js @@ -8,8 +8,8 @@ const RateLimit = require("../../middlewares/rate-limit"); const { asyncHandlerWrapper, requestValidation, -} = require("../../middlewares/apiUtils"); -const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quoteLanguages"); +} = require("../../middlewares/api-utils"); +const SUPPORTED_QUOTE_LANGUAGES = require("../../constants/quote-languages"); const quotesRouter = Router(); diff --git a/backend/constants/base-configuration.js b/backend/constants/base-configuration.js new file mode 100644 index 000000000..1ad105094 --- /dev/null +++ b/backend/constants/base-configuration.js @@ -0,0 +1,18 @@ +/** + * This is the base schema for the configuration of the API backend. + * 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 = { + maintenance: false, + quoteReport: { + enabled: false, + maxReports: 0, + contentReportLimit: 0, + }, + quoteSubmit: { + enabled: false, + }, +}; + +module.exports = BASE_CONFIGURATION; diff --git a/backend/constants/quoteLanguages.js b/backend/constants/quote-languages.js similarity index 100% rename from backend/constants/quoteLanguages.js rename to backend/constants/quote-languages.js diff --git a/backend/dao/configuration.js b/backend/dao/configuration.js new file mode 100644 index 000000000..2651201a4 --- /dev/null +++ b/backend/dao/configuration.js @@ -0,0 +1,84 @@ +const _ = require("lodash"); +const { mongoDB } = require("../init/mongodb"); +const BASE_CONFIGURATION = require("../constants/base-configuration"); +const Logger = require("../handlers/logger.js"); + +const CONFIG_UPDATE_INTERVAL = 10 * 60 * 1000; // 10 Minutes + +function mergeConfigurations(baseConfiguration, liveConfiguration) { + if ( + !_.isPlainObject(baseConfiguration) || + !_.isPlainObject(liveConfiguration) + ) { + return; + } + + function merge(base, source) { + const commonKeys = _.intersection(_.keys(base), _.keys(source)); + + commonKeys.forEach((key) => { + const baseValue = base[key]; + const sourceValue = source[key]; + + if (_.isPlainObject(baseValue) && _.isPlainObject(sourceValue)) { + merge(baseValue, sourceValue); + } else if (typeof baseValue === typeof sourceValue) { + base[key] = sourceValue; + } + }); + } + + merge(baseConfiguration, liveConfiguration); +} + +class ConfigurationDAO { + static configuration = Object.freeze(BASE_CONFIGURATION); + static lastFetchTime = 0; + + static async getCachedConfiguration(attemptCacheUpdate = false) { + if ( + attemptCacheUpdate && + this.lastFetchTime < Date.now() - CONFIG_UPDATE_INTERVAL + ) { + Logger.log("stale_configuration", "Cached configuration is stale."); + return await this.getLiveConfiguration(); + } + return this.configuration; + } + + static async getLiveConfiguration() { + this.lastFetchTime = Date.now(); + + try { + const liveConfiguration = await mongoDB() + .collection("configuration") + .findOne(); + + if (liveConfiguration) { + const baseConfiguration = _.cloneDeep(BASE_CONFIGURATION); + mergeConfigurations(baseConfiguration, liveConfiguration); + + this.configuration = baseConfiguration; + } else { + await mongoDB() + .collection("configuration") + .insertOne(BASE_CONFIGURATION); // Seed the base configuration. + } + Logger.log( + "fetch_configuration_success", + "Successfully fetched live configuration." + ); + } catch (error) { + Logger.log( + "fetch_configuration_failure", + `Could not fetch configuration: ${error.message}` + ); + } + + this.configuration = Object.freeze(this.configuration); + + return this.configuration; + } +} + +module.exports = ConfigurationDAO; diff --git a/backend/jobs/deleteOldLogs.js b/backend/jobs/delete-old-logs.js similarity index 100% rename from backend/jobs/deleteOldLogs.js rename to backend/jobs/delete-old-logs.js diff --git a/backend/jobs/index.js b/backend/jobs/index.js index 57bd1e358..2124f0f34 100644 --- a/backend/jobs/index.js +++ b/backend/jobs/index.js @@ -1,4 +1,4 @@ -const updateLeaderboards = require("./updateLeaderboards"); -const deleteOldLogs = require("./deleteOldLogs"); +const updateLeaderboards = require("./update-leaderboards"); +const deleteOldLogs = require("./delete-old-logs"); module.exports = [updateLeaderboards, deleteOldLogs]; diff --git a/backend/jobs/updateLeaderboards.js b/backend/jobs/update-leaderboards.js similarity index 100% rename from backend/jobs/updateLeaderboards.js rename to backend/jobs/update-leaderboards.js diff --git a/backend/middlewares/apiUtils.js b/backend/middlewares/api-utils.js similarity index 100% rename from backend/middlewares/apiUtils.js rename to backend/middlewares/api-utils.js diff --git a/backend/middlewares/context.js b/backend/middlewares/context.js new file mode 100644 index 000000000..a4fc313b9 --- /dev/null +++ b/backend/middlewares/context.js @@ -0,0 +1,13 @@ +const ConfigurationDAO = require("../dao/configuration"); + +async function contextMiddleware(req, res, next) { + const configuration = await ConfigurationDAO.getCachedConfiguration(true); + + req.context = { + configuration, + }; + + next(); +} + +module.exports = contextMiddleware; diff --git a/backend/server.js b/backend/server.js index 822d6a49f..a52fee2f6 100644 --- a/backend/server.js +++ b/backend/server.js @@ -10,6 +10,8 @@ const serviceAccount = require("./credentials/serviceAccountKey.json"); const { connectDB, mongoDB } = require("./init/mongodb"); const jobs = require("./jobs"); const addApiRoutes = require("./api/routes"); +const contextMiddleware = require("./middlewares/context"); +const ConfigurationDAO = require("./dao/configuration"); const PORT = process.env.PORT || 5005; @@ -21,8 +23,13 @@ app.use(cors()); app.set("trust proxy", 1); +app.use(contextMiddleware); + app.use((req, res, next) => { - if (process.env.MAINTENANCE === "true") { + if ( + process.env.MAINTENANCE === "true" || + req.context.configuration.maintenance + ) { res.status(503).json({ message: "Server is down for maintenance" }); } else { next(); @@ -79,6 +86,7 @@ app.listen(PORT, async () => { admin.initializeApp({ credential: admin.credential.cert(serviceAccount), }); + await ConfigurationDAO.getLiveConfiguration(); console.log("Starting cron jobs..."); jobs.forEach((job) => job.start()); diff --git a/package-lock.json b/package-lock.json index 455b09790..6139bce3a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8208,8 +8208,7 @@ "lodash": { "version": "4.17.21", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "dev": true + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash.camelcase": { "version": "4.3.0", diff --git a/package.json b/package.json index 4a3f97f3a..0a2049a39 100644 --- a/package.json +++ b/package.json @@ -7,6 +7,7 @@ "build": "npx gulp build", "start:dev": "npm run build && concurrently --kill-others \"npx gulp watch\" \"nodemon --watch ./backend ./backend/server.js\" \"firebase serve --only hosting\"", "start:dev:nodb": "npm run build && concurrently --kill-others \"npx gulp watch\" \"firebase serve --only hosting\"", + "start:dev:nofe": "nodemon --watch ./backend ./backend/server.js", "deploy:live": "npm run build && firebase deploy -P live --only hosting" }, "engines": { @@ -64,6 +65,7 @@ "helmet": "^4.6.0", "howler": "^2.2.1", "joi": "^17.6.0", + "lodash": "4.17.21", "moment-timezone": "^0.5.33", "mongodb": "^3.6.9", "node-fetch": "^2.6.7",