diff --git a/.github/CHANGELOG.md b/.github/CHANGELOG.md index 2cde2041..92a452b5 100644 --- a/.github/CHANGELOG.md +++ b/.github/CHANGELOG.md @@ -1,5 +1,11 @@ # Changelog +## ⚡️ 1.9.6 - Adds Proxy Support for Widget Requests [PR #392](https://github.com/Lissy93/dashy/pull/392) +- Refactors widget mixin to include data requests, so that code can be shared between widgets +- Adds a Node endpoint for proxying requests server-side, used for APIs that are not CORS enabled +- Adds option to config file for user to force proxying of requests +- Writes a Netlify cloud function to support proxying when the app is hosted on Netlify + ## 🐛 1.9.5 - Bug fixes and Minor Improvements [PR #388](https://github.com/Lissy93/dashy/pull/388) - Adds icon.horse to supported favicon APIs - Fixes tile move bug, Re: #366 diff --git a/docs/configuring.md b/docs/configuring.md index 23f8d186..bd975fda 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -172,7 +172,8 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)** --- | --- | --- | --- **`name`** | `string` | Required | The title for the section **`icon`** | `string` | _Optional_ | An single icon to be displayed next to the title. See [`section.icon`](#sectionicon-and-sectionitemicon) -**`items`** | `array` | Required | An array of items to be displayed within the section. See [`item`](#sectionitem) +**`items`** | `array` | _Optional_ | An array of items to be displayed within the section. See [`item`](#sectionitem). Sections must include either 1 or more items, or 1 or more widgets. +**`widgets`** | `array` | _Optional_ | An array of widgets to be displayed within the section. See [`widget`](#sectionwidget-optional) **`displayData`** | `object` | _Optional_ | Meta-data to optionally overide display settings for a given section. See [`displayData`](#sectiondisplaydata-optional) **[⬆️ Back to Top](#configuring)** @@ -198,6 +199,18 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)** **[⬆️ Back to Top](#configuring)** +### `section.widget` _(optional)_ + +**Field** | **Type** | **Required**| **Description** +--- | --- | --- | --- +**`type`** | `string` | Required | The widget type. See [Widget Docs](/docs/widgets.md) for full list of supported widgets +**`options`** | `object` | _Optional_ | Some widgets accept either optional or required additional options. Again, see the [Widget Docs](/docs/widgets.md) for full list of options +**`updateInterval`** | `number` | _Optional_ | You can keep a widget constantly updated by specifying an update interval, in seconds. See [Continuous Updates Docs](/docs/widgets.md#continuous-updates) for more info +**`useProxy`** | `boolean` | _Optional_ | Some widgets make API requests to services that are not CORS-enabled. For these instances, you will need to route requests through a proxy, Dashy has a built in CORS-proxy, which you can use by setting this option to `true`. Defaults to `false`. See the [Proxying Requests Docs](/docs/widgets.md#proxying-requests) for more info + +**[⬆️ Back to Top](#configuring)** + + ### `section.displayData` _(optional)_ **Field** | **Type** | **Required**| **Description** diff --git a/docs/widgets.md b/docs/widgets.md index dc962be8..badb7a5b 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -1208,6 +1208,31 @@ For more info on how to apply custom variables, see the [Theming Docs](/docs/the --- +### Proxying Requests + +If a widget fails to make a data request, and the console shows a CORS error, this means the server is blocking client-side requests. + +Dashy has a built-in CORS proxy ([`services/cors-proxy.js`](https://github.com/Lissy93/dashy/blob/master/services/cors-proxy.js)), which will be used automatically by some widgets, or can be forced to use by other by setting the `useProxy` option. + +For example: + +```yaml +widgets: +- type: pi-hole-stats + useProxy: true + options: + hostname: http://pi-hole.local +``` + +Alternativley, and more securley, you can set the auth headers on your service to accept requests from Dashy. For example: + +``` +Access-Control-Allow-Origin: https://location-of-dashy/ +Vary: Origin +``` + +--- + ### Language Translations Since most of the content displayed within widgets is fetched from an external API, unless that API supports multiple languages, translating dynamic content is not possible. diff --git a/netlify.toml b/netlify.toml index 6ed4983e..e0062796 100644 --- a/netlify.toml +++ b/netlify.toml @@ -1,42 +1,48 @@ -# Enables you to easily deploy a fork of Dashy to Netlify -# without the need to configure anything in admin UI -# Docs: https://www.netlify.com/docs/netlify-toml-reference/ - -# Essential site config -[build] - base = "/" - command = "yarn build" - publish = "dist" - functions = "services/serverless-functions" - -# Site info, used for the 1-Click deploy page -[template.environment] - STATUSKIT_PAGE_TITLE = "Dashy" - STATUSKIT_COMPANY_LOGO = "https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/logo.png" - STATUSKIT_SUPPORT_CONTACT_LINK = "https://github.com/lissy93/dashy" - STATUSKIT_RESOURCES_LINK = "https://dashy.to/docs" - -# Redirect the Node endpoints to serverless functions -[[redirects]] - from = "/status-check" - to = "/.netlify/functions/cloud-status-check" - status = 301 - force = true -[[redirects]] - from = "/config-manager/*" - to = "/.netlify/functions/not-supported" - status = 301 - force = true - -# For router history mode, ensure pages land on index -[[redirects]] - from = "/*" - to = "/index.html" - status = 200 - -# Set any security headers here -[[headers]] - for = "/*" - [headers.values] - # Uncomment to enable Netlify user control. You must have a paid plan. - # Basic-Auth = "someuser:somepassword anotheruser:anotherpassword" +# Enables you to easily deploy a fork of Dashy to Netlify +# without the need to configure anything in admin UI +# Docs: https://www.netlify.com/docs/netlify-toml-reference/ + +# Essential site config +[build] + base = "/" + command = "yarn build" + publish = "dist" + functions = "services/serverless-functions" + +# Site info, used for the 1-Click deploy page +[template.environment] + STATUSKIT_PAGE_TITLE = "Dashy" + STATUSKIT_COMPANY_LOGO = "https://raw.githubusercontent.com/Lissy93/dashy/master/docs/assets/logo.png" + STATUSKIT_SUPPORT_CONTACT_LINK = "https://github.com/lissy93/dashy" + STATUSKIT_RESOURCES_LINK = "https://dashy.to/docs" + +# Redirect the Node endpoints to serverless functions +[[redirects]] + from = "/status-check" + to = "/.netlify/functions/cloud-status-check" + status = 301 + force = true +[[redirects]] + from = "/config-manager/*" + to = "/.netlify/functions/not-supported" + status = 301 + force = true +[[redirects]] + from = "/cors-proxy" + to = "/.netlify/functions/netlify-cors" + status = 301 + force = true + +# For router history mode, ensure pages land on index +[[redirects]] + from = "/*" + to = "/index.html" + status = 200 + +# Set any security headers here +[[headers]] + for = "/*" + [headers.values] + # Uncomment to enable Netlify user control. You must have a paid plan. + # Basic-Auth = "someuser:somepassword anotheruser:anotherpassword" + diff --git a/package.json b/package.json index ca3a3a6b..0a26e47c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "Dashy", - "version": "1.9.5", + "version": "1.9.6", "license": "MIT", "main": "server", "author": "Alicia Sykes (https://aliciasykes.com)", diff --git a/services/cors-proxy.js b/services/cors-proxy.js index cb2d264f..97ac63b3 100644 --- a/services/cors-proxy.js +++ b/services/cors-proxy.js @@ -32,7 +32,7 @@ module.exports = (req, res) => { // Prepare the request const requestConfig = { method: req.method, - url: targetURL + req.url, + url: targetURL, json: req.body, headers, }; diff --git a/services/serverless-functions/netlify-cors.js b/services/serverless-functions/netlify-cors.js new file mode 100644 index 00000000..3789238e --- /dev/null +++ b/services/serverless-functions/netlify-cors.js @@ -0,0 +1,48 @@ +/* A Netlify cloud function to handle requests to CORS-disabled services */ +const axios = require('axios'); + +exports.handler = (event, context, callback) => { + // Get input data + const { body, headers, queryStringParameters } = event; + + // Get URL from header or GET param + const requestUrl = queryStringParameters.url || headers['Target-URL'] || headers['target-url']; + + const returnError = (msg, error) => { + callback(null, { + statusCode: 400, + body: JSON.stringify({ success: false, msg, error }), + }); + }; + // If URL missing, return error + if (!requestUrl) { + returnError('Missing Target-URL header', null); + } + + let custom = {}; + try { + custom = JSON.parse(headers.CustomHeaders || headers.customheaders || '{}'); + } catch (e) { returnError('Unable to parse custom headers'); } + + // Response headers + const requestHeaders = { + 'Access-Control-Allow-Origin': '*', + ...custom, + }; + + // Prepare request + const requestConfig = { + method: 'GET', + url: requestUrl, + json: body, + headers: requestHeaders, + }; + + // Make request + axios.request(requestConfig) + .then((response) => { + callback(null, { statusCode: 200, body: JSON.stringify(response.data) }); + }).catch((error) => { + returnError('Request failed', error); + }); +}; diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 72d370d0..70713065 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -263,6 +263,12 @@ "up": "Online", "down": "Offline" }, + "net-data": { + "cpu-chart-title": "CPU History", + "mem-chart-title": "Memory Usage", + "mem-breakdown-title": "Memory Breakdown", + "load-chart-title": "System Load" + }, "system-info": { "uptime": "Uptime" }, diff --git a/src/components/Widgets/HealthChecks.vue b/src/components/Widgets/HealthChecks.vue index 6c8b20b9..1243eb5c 100644 --- a/src/components/Widgets/HealthChecks.vue +++ b/src/components/Widgets/HealthChecks.vue @@ -17,9 +17,8 @@