From 856b9eb3c6b611af3cfb71f23a903816d04ebec2 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 18 Jun 2021 14:14:31 +0100 Subject: [PATCH 01/20] Refactors the main server.js, and moves welcome msg to separate file --- server.js | 84 +++++++++++++++------------------------ services/print-message.js | 42 ++++++++++++++++++++ 2 files changed, 75 insertions(+), 51 deletions(-) create mode 100644 services/print-message.js diff --git a/server.js b/server.js index d1401c47a1..06594cbca9 100644 --- a/server.js +++ b/server.js @@ -1,84 +1,66 @@ -/* eslint-disable no-console */ -/* This is a simple Node.js http server, that is used to serve up the contents of ./dist */ -const connect = require('connect'); -const serveStatic = require('serve-static'); +/** + * Note: The app must first be built (yarn build) before this script is run + * This is the main entry point for the application, a simple server that + * runs some checks, and then serves up the app from the ./dist directory + * Also includes some routes for status checks/ ping and config saving + * */ +/* Include required node dependencies */ +const serveStatic = require('serve-static'); +const connect = require('connect'); const util = require('util'); const dns = require('dns'); const os = require('os'); -require('./src/utils/ConfigValidator'); - -const pingUrl = require('./services/ping'); +/* Include helper functions */ +const pingUrl = require('./services/ping'); // Used by the status check feature, to ping services +const printMessage = require('./services/print-message'); // Function to print welcome msg on start +require('./src/utils/ConfigValidator'); // Include and kicks off the config file validation script +/* Checks if app is running within a container, from env var */ const isDocker = !!process.env.IS_DOCKER; /* Checks env var for port. If undefined, will use Port 80 for Docker, or 4000 for metal */ const port = process.env.PORT || (isDocker ? 80 : 4000); +/* Attempts to get the users local IP, used as part of welcome message */ const getLocalIp = () => { const dnsLookup = util.promisify(dns.lookup); return dnsLookup(os.hostname()); }; -const overComplicatedMessage = (ip) => { - let msg = ''; - const chars = { - RESET: '\x1b[0m', - CYAN: '\x1b[36m', - GREEN: '\x1b[32m', - BLUE: '\x1b[34m', - BRIGHT: '\x1b[1m', - BR: '\n', - }; - const stars = (count) => new Array(count).fill('*').join(''); - const line = (count) => new Array(count).fill('━').join(''); - const blanks = (count) => new Array(count).fill(' ').join(''); - if (isDocker) { - const containerId = process.env.HOSTNAME || undefined; - msg = `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}` - + `${chars.CYAN}Welcome to Dashy! πŸš€${chars.RESET}${chars.BR}` - + `${chars.GREEN}Your new dashboard is now up and running ` - + `${containerId ? `in container ID ${containerId}` : 'with Docker'}${chars.BR}` - + `${chars.GREEN}After updating your config file, run ` - + `'${chars.BRIGHT}docker exec -it ${containerId || '[container-id]'} yarn build` - + `${chars.RESET}${chars.GREEN}' to rebuild${chars.BR}` - + `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`; - } else { - msg = `${chars.GREEN}┏${line(75)}β”“${chars.BR}` - + `┃ ${chars.CYAN}Welcome to Dashy! πŸš€${blanks(55)}${chars.GREEN}┃${chars.BR}` - + `┃ ${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}` - + `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}┃${chars.BR}` - + `┃ ${chars.CYAN}After updating your config file, run '${chars.BRIGHT}yarn build` - + `${chars.RESET}${chars.CYAN}' to rebuild the app${blanks(6)}${chars.GREEN}┃${chars.BR}` - + `β”—${line(75)}β”›${chars.BR}${chars.BR}`; - } - return msg; -}; - -/* eslint no-console: 0 */ +/* Gets the users local IP and port, then calls to print welcome message */ const printWelcomeMessage = () => { getLocalIp().then(({ address }) => { const ip = address || 'localhost'; - console.log(overComplicatedMessage(ip)); + console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console }); }; +const printWarning = (msg, error) => { + console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console +}; + try { connect() - .use(serveStatic(`${__dirname}/dist`)) /* Serves up the main built application to the root */ - .use(serveStatic(`${__dirname}/public`, { index: 'default.html' })) /* During build, a custom page will be served */ - .use('/ping', (req, res) => { /* This root returns the status of a given service - used for uptime monitoring */ + // Serves up the main built application to the root + .use(serveStatic(`${__dirname}/dist`)) + // During build, a custom page will be served before the app is available + .use(serveStatic(`${__dirname}/public`, { index: 'default.html' })) + // This root returns the status of a given service - used for uptime monitoring + .use('/ping', (req, res) => { try { pingUrl(req.url, async (results) => { await res.end(results); }); - // next(); - } catch (e) { console.warn(`Error running ping check for ${req.url}\n`, e); } + } catch (e) { + printWarning(`Error running ping check for ${req.url}\n`, e); + } }) + // Finally, initialize the server then print welcome message .listen(port, () => { - try { printWelcomeMessage(); } catch (e) { console.log('Dashy is Starting...'); } + try { printWelcomeMessage(); } catch (e) { printWarning('Dashy is Starting...'); } }); } catch (error) { - console.log('Sorry, an error occurred ', error); + printWarning('Sorry, an error occurred ', error); } diff --git a/services/print-message.js b/services/print-message.js new file mode 100644 index 0000000000..e2823c2f64 --- /dev/null +++ b/services/print-message.js @@ -0,0 +1,42 @@ +/** + * A function that prints a welcome message to the user when they start the app + * Contains essential info about restarting and managing the container or service + * @param String ip: The users local IP address + * @param Integer port: the port number the app is running at + * @param Boolean isDocker: whether or not the app is being run within a container + * @returns A string formatted for the terminal + */ +module.exports = (ip, port, isDocker) => { + let msg = ''; + const chars = { + RESET: '\x1b[0m', + CYAN: '\x1b[36m', + GREEN: '\x1b[32m', + BLUE: '\x1b[34m', + BRIGHT: '\x1b[1m', + BR: '\n', + }; + const stars = (count) => new Array(count).fill('*').join(''); + const line = (count) => new Array(count).fill('━').join(''); + const blanks = (count) => new Array(count).fill(' ').join(''); + if (isDocker) { + const containerId = process.env.HOSTNAME || undefined; + msg = `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}` + + `${chars.CYAN}Welcome to Dashy! πŸš€${chars.RESET}${chars.BR}` + + `${chars.GREEN}Your new dashboard is now up and running ` + + `${containerId ? `in container ID ${containerId}` : 'with Docker'}${chars.BR}` + + `${chars.GREEN}After updating your config file, run ` + + `'${chars.BRIGHT}docker exec -it ${containerId || '[container-id]'} yarn build` + + `${chars.RESET}${chars.GREEN}' to rebuild${chars.BR}` + + `${chars.BLUE}${stars(91)}${chars.BR}${chars.RESET}`; + } else { + msg = `${chars.GREEN}┏${line(75)}β”“${chars.BR}` + + `┃ ${chars.CYAN}Welcome to Dashy! πŸš€${blanks(55)}${chars.GREEN}┃${chars.BR}` + + `┃ ${chars.CYAN}Your new dashboard is now up and running at ${chars.BRIGHT}` + + `http://${ip}:${port}${chars.RESET}${blanks(18 - ip.length)}${chars.GREEN}┃${chars.BR}` + + `┃ ${chars.CYAN}After updating your config file, run '${chars.BRIGHT}yarn build` + + `${chars.RESET}${chars.CYAN}' to rebuild the app${blanks(6)}${chars.GREEN}┃${chars.BR}` + + `β”—${line(75)}β”›${chars.BR}${chars.BR}`; + } + return msg; +}; From 77ca662f377884402e896f0c0a7a4e26deb6f471 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Fri, 18 Jun 2021 18:01:42 +0100 Subject: [PATCH 02/20] Adds server endpoint for backing up and saving conf.yml. Still needs some improvments though. --- package.json | 3 +- server.js | 14 ++++++++ services/save-config.js | 77 +++++++++++++++++++++++++++++++++++++++++ yarn.lock | 2 +- 4 files changed, 94 insertions(+), 2 deletions(-) create mode 100644 services/save-config.js diff --git a/package.json b/package.json index 837062196f..f92df0bb7c 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "dependencies": { "ajv": "^8.5.0", "axios": "^0.21.1", + "body-parser": "^1.19.0", "connect": "^3.7.0", "crypto-js": "^4.0.0", "highlight.js": "^11.0.0", @@ -83,4 +84,4 @@ "> 1%", "last 2 versions" ] -} \ No newline at end of file +} diff --git a/server.js b/server.js index 06594cbca9..3bd98f7435 100644 --- a/server.js +++ b/server.js @@ -11,9 +11,11 @@ const connect = require('connect'); const util = require('util'); const dns = require('dns'); const os = require('os'); +const bodyParser = require('body-parser'); /* Include helper functions */ const pingUrl = require('./services/ping'); // Used by the status check feature, to ping services +const saveConfig = require('./services/save-config'); // Saves users new conf.yml to file-system const printMessage = require('./services/print-message'); // Function to print welcome msg on start require('./src/utils/ConfigValidator'); // Include and kicks off the config file validation script @@ -41,8 +43,12 @@ const printWarning = (msg, error) => { console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console }; +/* A middleware function for Connect, that filters requests based on method type */ +const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next()); + try { connect() + .use(bodyParser.json()) // Serves up the main built application to the root .use(serveStatic(`${__dirname}/dist`)) // During build, a custom page will be served before the app is available @@ -57,6 +63,14 @@ try { printWarning(`Error running ping check for ${req.url}\n`, e); } }) + .use('/api', method('GET', (req, res) => res.end('hi!'))) + // POST Endpoint used to save config, by writing conf.yml to disk + .use('/api/save', method('POST', (req, res) => { + saveConfig(req.body, async (results) => { + await res.end(results); + }); + // res.end('Will Save'); + })) // Finally, initialize the server then print welcome message .listen(port, () => { try { printWelcomeMessage(); } catch (e) { printWarning('Dashy is Starting...'); } diff --git a/services/save-config.js b/services/save-config.js new file mode 100644 index 0000000000..4ce8ab2c58 --- /dev/null +++ b/services/save-config.js @@ -0,0 +1,77 @@ +const fs = require('fs').promises; + +/* Copies an existing file to a new file */ +async function backupConfig(fromPath, toPath, done) { + try { + fs.copyFile(fromPath, toPath, done({ success: true })); + } catch (error) { + done({ + success: false, + message: `Error backing up config file: ${error.message}`, + }); + } +} + +/* Creates a new file and writes content to it */ +async function saveNewConfig(writePath, fileContents, done) { + try { + fs.writeFile(writePath, fileContents, done({ success: true })); + } catch (error) { + done({ + success: false, + message: `Error writing changes to config file: ${error.message}`, + }); + } +} + +module.exports = (newConfig, render) => { + // Define constants for the config file + const settings = { + defaultLocation: './public/', + defaultFile: 'conf.yml', + filename: 'conf', + backupDenominator: '.backup.yml', + }; + + // Make the full file name and path to save the backup config file + const backupFilePath = `${settings.defaultLocation}${settings.filename}-` + + `${Math.round(new Date() / 1000)}${settings.backupDenominator}`; + + // The path where the main conf.yml should be read and saved to + const defaultFilePath = settings.defaultLocation + settings.defaultFile; + + // Returns a string confirming successful job + const getSuccessMessage = () => `Successfully backed up ${settings.defaultFile} to` + + ` ${backupFilePath}, and updated the contents of ${defaultFilePath}`; + + // Prepare the response returned by the API + const getRenderMessage = (success, errorMsg) => JSON.stringify({ + success, + message: !success ? errorMsg : getSuccessMessage(), + }); + + // Backs up the config, then writes new content to the existing config, and returns + backupConfig(defaultFilePath, backupFilePath, (backupResult) => { + if (!backupResult.success) { + render(getRenderMessage(false, backupResult.message)); + } else { + saveNewConfig(defaultFilePath, newConfig.config, (copyResult) => { + if (copyResult.failed) render(getRenderMessage(false, copyResult.message)); + render(getRenderMessage(true)); + }); + } + }); + + // Promise.resolve().then(() => { + // backupConfig(defaultFilePath, backupFilePath) + // .catch(error => thereWasAnError(error)); + // }).then(() => { + // saveNewConfig(defaultFilePath, newConfig) + // .catch(error => thereWasAnError(error)); + // }).then(() => { + // render(JSON.stringify({ + // success: !failed, + // message: failed ? errorMessage : 'Success!', + // })); + // }); +}; diff --git a/yarn.lock b/yarn.lock index e067356ee6..4978ef70f2 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2112,7 +2112,7 @@ bn.js@^5.0.0, bn.js@^5.1.1: resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-5.2.0.tgz#358860674396c6997771a9d051fcc1b57d4ae002" integrity sha512-D7iWRBvnZE8ecXiLj/9wbxH7Tk79fAh8IHaTNq1RWRixsS02W+5qS+iE9yq6RYl0asXx5tw0bLhmT5pIfbSquw== -body-parser@1.19.0: +body-parser@1.19.0, body-parser@^1.19.0: version "1.19.0" resolved "https://registry.yarnpkg.com/body-parser/-/body-parser-1.19.0.tgz#96b2709e57c9c4e09a6fd66a8fd979844f69f08a" integrity sha512-dhEPs72UPbDnAQJ9ZKMNTP6ptJaionhP5cBb541nXPlW60Jepo9RV/a4fX4XWW9CuFNK22krhrj1+rgzifNCsw== From 106103a7df1c1ce6b188f17f131e5c7d385bffb5 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 19 Jun 2021 13:46:02 +0100 Subject: [PATCH 03/20] Syntactic improvments, and linting ping.js --- services/ping.js | 132 +++++++++++++++++++++++------------------------ 1 file changed, 66 insertions(+), 66 deletions(-) diff --git a/services/ping.js b/services/ping.js index 43e5caa4f6..35e273e2e3 100644 --- a/services/ping.js +++ b/services/ping.js @@ -1,66 +1,66 @@ -/** - * This file contains the Node.js code, used for the optional status check feature - * It accepts a single url parameter, and will make an empty GET request to that - * endpoint, and then resolve the response status code, time taken, and short message - */ -const axios = require('axios').default; - -/* Determines if successful from the HTTP response code */ -const getResponseType = (code) => { - if (Number.isNaN(code)) return false; - const numericCode = parseInt(code, 10); - return (numericCode >= 200 && numericCode <= 302); -}; - -/* Makes human-readable response text for successful check */ -const makeMessageText = (data) => `${data.successStatus ? 'βœ…' : '⚠️'} ` - + `${data.serverName || 'Server'} responded with ` - + `${data.statusCode} - ${data.statusText}. ` - + `\n⏱️Took ${data.timeTaken} ms`; - -/* Makes human-readable response text for failed check */ -const makeErrorMessage = (data) => `❌ Service Unavailable: ${data.hostname || 'Server'} ` - + `resulted in ${data.code || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`; - -const makeErrorMessage2 = (data) => `❌ Service Error - ` - + `${data.status} - ${data.statusText}`; - -/* Kicks of a HTTP request, then formats and renders results */ -const makeRequest = (url, render) => { - const startTime = new Date(); - axios.get(url) - .then((response) => { - const statusCode = response.status; - const { statusText } = response; - const successStatus = getResponseType(statusCode); - const serverName = response.request.socket.servername; - const timeTaken = (new Date() - startTime); - const results = { - statusCode, statusText, serverName, successStatus, timeTaken, - }; - const messageText = makeMessageText(results); - results.message = messageText; - return results; - }) - .catch((error) => { - render(JSON.stringify({ - successStatus: false, - message: error.response ? makeErrorMessage2(error.response) : makeErrorMessage(error), - })); - }).then((results) => { - render(JSON.stringify(results)); - }); -}; - -/* Main function, will check if a URL present, and call function */ -module.exports = (params, render) => { - if (!params || !params.includes('=')) { - render(JSON.stringify({ - success: false, - message: '❌ Malformed URL', - })); - } else { - const url = params.split('=')[1]; - makeRequest(url, render); - } -}; +/** + * This file contains the Node.js code, used for the optional status check feature + * It accepts a single url parameter, and will make an empty GET request to that + * endpoint, and then resolve the response status code, time taken, and short message + */ +const axios = require('axios').default; + +/* Determines if successful from the HTTP response code */ +const getResponseType = (code) => { + if (Number.isNaN(code)) return false; + const numericCode = parseInt(code, 10); + return (numericCode >= 200 && numericCode <= 302); +}; + +/* Makes human-readable response text for successful check */ +const makeMessageText = (data) => `${data.successStatus ? 'βœ…' : '⚠️'} ` + + `${data.serverName || 'Server'} responded with ` + + `${data.statusCode} - ${data.statusText}. ` + + `\n⏱️Took ${data.timeTaken} ms`; + +/* Makes human-readable response text for failed check */ +const makeErrorMessage = (data) => `❌ Service Unavailable: ${data.hostname || 'Server'} ` + + `resulted in ${data.code || 'a fatal error'} ${data.errno ? `(${data.errno})` : ''}`; + +const makeErrorMessage2 = (data) => '❌ Service Error - ' + + `${data.status} - ${data.statusText}`; + +/* Kicks of a HTTP request, then formats and renders results */ +const makeRequest = (url, render) => { + const startTime = new Date(); + axios.get(url) + .then((response) => { + const statusCode = response.status; + const { statusText } = response; + const successStatus = getResponseType(statusCode); + const serverName = response.request.socket.servername; + const timeTaken = (new Date() - startTime); + const results = { + statusCode, statusText, serverName, successStatus, timeTaken, + }; + const messageText = makeMessageText(results); + results.message = messageText; + return results; + }) + .catch((error) => { + render(JSON.stringify({ + successStatus: false, + message: error.response ? makeErrorMessage2(error.response) : makeErrorMessage(error), + })); + }).then((results) => { + render(JSON.stringify(results)); + }); +}; + +/* Main function, will check if a URL present, and call function */ +module.exports = (params, render) => { + if (!params || !params.includes('=')) { + render(JSON.stringify({ + success: false, + message: '❌ Malformed URL', + })); + } else { + const url = params.split('=')[1]; + makeRequest(url, render); + } +}; From 760c464c19df731130311d56ccc1ebc4642d04cd Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 19 Jun 2021 13:47:10 +0100 Subject: [PATCH 04/20] Got the save config route working in the node server --- server.js | 12 ++++--- services/save-config.js | 75 ++++++++++++++--------------------------- 2 files changed, 32 insertions(+), 55 deletions(-) diff --git a/server.js b/server.js index 3bd98f7435..3518364053 100644 --- a/server.js +++ b/server.js @@ -63,13 +63,15 @@ try { printWarning(`Error running ping check for ${req.url}\n`, e); } }) - .use('/api', method('GET', (req, res) => res.end('hi!'))) // POST Endpoint used to save config, by writing conf.yml to disk .use('/api/save', method('POST', (req, res) => { - saveConfig(req.body, async (results) => { - await res.end(results); - }); - // res.end('Will Save'); + try { + saveConfig(req.body, (results) => { + res.end(results); + }); + } catch (e) { + res.end(JSON.stringify({ success: false, message: e })); + } })) // Finally, initialize the server then print welcome message .listen(port, () => { diff --git a/services/save-config.js b/services/save-config.js index 4ce8ab2c58..bc74c20132 100644 --- a/services/save-config.js +++ b/services/save-config.js @@ -1,30 +1,12 @@ -const fs = require('fs').promises; - -/* Copies an existing file to a new file */ -async function backupConfig(fromPath, toPath, done) { - try { - fs.copyFile(fromPath, toPath, done({ success: true })); - } catch (error) { - done({ - success: false, - message: `Error backing up config file: ${error.message}`, - }); - } -} - -/* Creates a new file and writes content to it */ -async function saveNewConfig(writePath, fileContents, done) { - try { - fs.writeFile(writePath, fileContents, done({ success: true })); - } catch (error) { - done({ - success: false, - message: `Error writing changes to config file: ${error.message}`, - }); - } -} - -module.exports = (newConfig, render) => { +/** + * This file exports a function, used by the write config endpoint. + * It will make a backup of the users conf.yml file + * and then write their new config into the main conf.yml file. + * Finally, it will call a function with the status message + */ +const fsPromises = require('fs').promises; + +module.exports = async (newConfig, render) => { // Define constants for the config file const settings = { defaultLocation: './public/', @@ -44,34 +26,27 @@ module.exports = (newConfig, render) => { const getSuccessMessage = () => `Successfully backed up ${settings.defaultFile} to` + ` ${backupFilePath}, and updated the contents of ${defaultFilePath}`; + // Encoding options for writing to conf file + const writeFileOptions = { encoding: 'utf8' }; + // Prepare the response returned by the API const getRenderMessage = (success, errorMsg) => JSON.stringify({ success, message: !success ? errorMsg : getSuccessMessage(), }); - // Backs up the config, then writes new content to the existing config, and returns - backupConfig(defaultFilePath, backupFilePath, (backupResult) => { - if (!backupResult.success) { - render(getRenderMessage(false, backupResult.message)); - } else { - saveNewConfig(defaultFilePath, newConfig.config, (copyResult) => { - if (copyResult.failed) render(getRenderMessage(false, copyResult.message)); - render(getRenderMessage(true)); - }); - } - }); + // Makes a backup of the existing config file + await fsPromises.copyFile(defaultFilePath, backupFilePath) + .catch((error) => { + render(getRenderMessage(false, `Unable to backup conf.yml: ${error}`)); + }); + + // Writes the new content to the conf.yml file + await fsPromises.writeFile(defaultFilePath, newConfig.config.toString(), writeFileOptions) + .catch((error) => { + render(getRenderMessage(false, `Unable to write changes to conf.yml: ${error}`)); + }); - // Promise.resolve().then(() => { - // backupConfig(defaultFilePath, backupFilePath) - // .catch(error => thereWasAnError(error)); - // }).then(() => { - // saveNewConfig(defaultFilePath, newConfig) - // .catch(error => thereWasAnError(error)); - // }).then(() => { - // render(JSON.stringify({ - // success: !failed, - // message: failed ? errorMessage : 'Success!', - // })); - // }); + // If successful, then render hasn't yet been called- call it + await render(getRenderMessage(true)); }; From a954f8c0fbdb551281ccbefa44a5c926007e6d0b Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 19 Jun 2021 13:54:48 +0100 Subject: [PATCH 05/20] Adds new property, `appConfig.allowConfigEdit`, in order to allow / prevent the user from writing changes to the conf file from the UI --- docs/configuring.md | 1 + src/utils/ConfigSchema.json | 5 +++++ 2 files changed, 6 insertions(+) diff --git a/docs/configuring.md b/docs/configuring.md index 938f52c056..077682ec08 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -58,6 +58,7 @@ All fields are optional, unless otherwise stated. **`customCss`** | `string` | _Optional_ | Raw CSS that will be applied to the page. This can also be set from the UI. Please minify it first. **`showSplashScreen`** | `boolean` | _Optional_ | Should display a splash screen while the app is loading. Defaults to false, except on first load **`auth`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. Note authentication is done on the client side, and so if your instance of Dashy is exposed to the internet, it is recommend to configure your web server to handle this. See [`auth`](#appconfigauth-optional) +**`allowConfigEdit`** | `boolean` | _Optional_ | Should prevent / allow the user to write configuration changes to the conf.yml from the UI. When set to `false`, the user can only apply changes locally using the config editor within the app, whereas if set to `true` then changes can be written to disk directly through the UI. Defaults to `true`. Note that if authentication is enabled, the user must be of type `admin` in order to apply changes globally. **[⬆️ Back to Top](#configuring)** diff --git a/src/utils/ConfigSchema.json b/src/utils/ConfigSchema.json index 1f458dfa1b..1b89dc8ba4 100644 --- a/src/utils/ConfigSchema.json +++ b/src/utils/ConfigSchema.json @@ -150,6 +150,11 @@ } } } + }, + "allowConfigEdit": { + "type": "boolean", + "default": true, + "description": "Can user write changes to conf.yml file from the UI. If set to false, preferences are only stored locally" } }, "additionalProperties": false From b0d5b6370313ae6eda03a1ac08977e66a976490f Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sat, 19 Jun 2021 19:21:32 +0100 Subject: [PATCH 06/20] Adds an endpoint for rebuilding the app, so that it can be triggered from the UI --- server.js | 16 +++++++++++++--- services/rebuild-app.js | 31 +++++++++++++++++++++++++++++++ 2 files changed, 44 insertions(+), 3 deletions(-) create mode 100644 services/rebuild-app.js diff --git a/server.js b/server.js index 3518364053..102cbb258c 100644 --- a/server.js +++ b/server.js @@ -13,10 +13,11 @@ const dns = require('dns'); const os = require('os'); const bodyParser = require('body-parser'); -/* Include helper functions */ +/* Include helper functions and route handlers */ const pingUrl = require('./services/ping'); // Used by the status check feature, to ping services const saveConfig = require('./services/save-config'); // Saves users new conf.yml to file-system const printMessage = require('./services/print-message'); // Function to print welcome msg on start +const rebuild = require('./services/rebuild-app'); // A script to programmatically trigger a build require('./src/utils/ConfigValidator'); // Include and kicks off the config file validation script /* Checks if app is running within a container, from env var */ @@ -39,6 +40,7 @@ const printWelcomeMessage = () => { }); }; +/* Just console.warns an error */ const printWarning = (msg, error) => { console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console }; @@ -64,7 +66,7 @@ try { } }) // POST Endpoint used to save config, by writing conf.yml to disk - .use('/api/save', method('POST', (req, res) => { + .use('/config-manager/save', method('POST', (req, res) => { try { saveConfig(req.body, (results) => { res.end(results); @@ -73,10 +75,18 @@ try { res.end(JSON.stringify({ success: false, message: e })); } })) + // GET endpoint to trigger a build, and respond with success status and output + .use('/config-manager/rebuild', (req, res) => { + rebuild().then((response) => { + res.end(JSON.stringify(response)); + }).catch((response) => { + res.end(JSON.stringify(response)); + }); + }) // Finally, initialize the server then print welcome message .listen(port, () => { try { printWelcomeMessage(); } catch (e) { printWarning('Dashy is Starting...'); } }); } catch (error) { - printWarning('Sorry, an error occurred ', error); + printWarning('Sorry, a critical error occurred ', error); } diff --git a/services/rebuild-app.js b/services/rebuild-app.js new file mode 100644 index 0000000000..9d00156a35 --- /dev/null +++ b/services/rebuild-app.js @@ -0,0 +1,31 @@ +/** + * This script programmatically triggers a production build + * and responds with the status, message and full output + */ +const { exec } = require('child_process'); + +module.exports = () => new Promise((resolve, reject) => { + const buildProcess = exec('npm run build'); + + let output = ''; + + buildProcess.stdout.on('data', (data) => { + process.stdout.write(data); + output += data; + }); + + buildProcess.on('error', (error) => { + reject(Error({ + success: false, + error, + output, + })); + }); + + buildProcess.on('exit', (response) => { + const success = response === 0; + const message = `Build process exited with ${response}: ` + + `${success ? 'Success' : 'Possible Error'}`; + resolve({ success, message, output }); + }); +}); From e75b0c780f81e8215fb5ebf943b16a4679d337d9 Mon Sep 17 00:00:00 2001 From: Alicia Sykes Date: Sun, 20 Jun 2021 16:51:23 +0100 Subject: [PATCH 07/20] =?UTF-8?q?=E2=9C=A8=20Implements=20frontend=20work?= =?UTF-8?q?=20for=20Rebuild=20App=20functionality?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../interface-icons/application-rebuild.svg | 1 + .../interface-icons/application-reload.svg | 1 + src/assets/interface-icons/loader.svg | 33 ++++ .../Configuration/ConfigContainer.vue | 20 ++- src/components/Configuration/RebuildApp.vue | 169 ++++++++++++++++++ src/utils/defaults.js | 1 + 6 files changed, 222 insertions(+), 3 deletions(-) create mode 100644 src/assets/interface-icons/application-rebuild.svg create mode 100644 src/assets/interface-icons/application-reload.svg create mode 100644 src/assets/interface-icons/loader.svg create mode 100644 src/components/Configuration/RebuildApp.vue diff --git a/src/assets/interface-icons/application-rebuild.svg b/src/assets/interface-icons/application-rebuild.svg new file mode 100644 index 0000000000..7f2c709533 --- /dev/null +++ b/src/assets/interface-icons/application-rebuild.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/interface-icons/application-reload.svg b/src/assets/interface-icons/application-reload.svg new file mode 100644 index 0000000000..22d63563a1 --- /dev/null +++ b/src/assets/interface-icons/application-reload.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/assets/interface-icons/loader.svg b/src/assets/interface-icons/loader.svg new file mode 100644 index 0000000000..cc25c0fd94 --- /dev/null +++ b/src/assets/interface-icons/loader.svg @@ -0,0 +1,33 @@ + + + + + + + + + + + \ No newline at end of file diff --git a/src/components/Configuration/ConfigContainer.vue b/src/components/Configuration/ConfigContainer.vue index 3146db521f..a36a1b8574 100644 --- a/src/components/Configuration/ConfigContainer.vue +++ b/src/components/Configuration/ConfigContainer.vue @@ -11,7 +11,7 @@ +

diff --git a/src/components/FormElements/Button.vue b/src/components/FormElements/Button.vue index 5867463421..f6c498f624 100644 --- a/src/components/FormElements/Button.vue +++ b/src/components/FormElements/Button.vue @@ -1,5 +1,5 @@