From 16a0e2292ea1b9196840b926903960eef62ba5a0 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 30 Sep 2021 11:47:15 -0700 Subject: [PATCH 01/10] Add global mixin for fetchOptions --- src/components/services/AdGuardHome.vue | 14 ++++++++------ src/components/services/Mealie.vue | 8 ++++---- src/components/services/Medusa.vue | 10 ++++++---- src/components/services/OpenWeather.vue | 2 +- src/components/services/PaperlessNG.vue | 5 ++--- src/components/services/PiHole.vue | 5 ++--- src/components/services/Ping.vue | 5 ++--- src/components/services/Radarr.vue | 8 ++------ src/components/services/Sonarr.vue | 8 ++------ src/main.js | 15 +++++++++++++++ 10 files changed, 44 insertions(+), 36 deletions(-) diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue index 61d4bed91..e16928cad 100644 --- a/src/components/services/AdGuardHome.vue +++ b/src/components/services/AdGuardHome.vue @@ -74,16 +74,18 @@ export default { }, methods: { fetchStatus: async function () { - this.status = await fetch(`${this.item.url}/control/status`, { - credentials: "include", - }) + this.status = await fetch( + `${this.item.url}/control/status`, + this.fetchOptions() + ) .then((response) => response.json()) .catch((e) => console.log(e)); }, fetchStats: async function () { - this.stats = await fetch(`${this.item.url}/control/stats`, { - credentials: "include", - }) + this.stats = await fetch( + `${this.item.url}/control/stats`, + this.fetchOptions() + ) .then((response) => response.json()) .catch((e) => console.log(e)); }, diff --git a/src/components/services/Mealie.vue b/src/components/services/Mealie.vue index 790a9b11f..0c439bfa1 100644 --- a/src/components/services/Mealie.vue +++ b/src/components/services/Mealie.vue @@ -52,12 +52,12 @@ export default { methods: { fetchStatus: async function () { if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing - this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, { + this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, this.fetchOptions({ headers: { Authorization: "Bearer " + this.item.apikey, Accept: "application/json", }, - }) + })) .then(function (response) { if (!response.ok) { throw new Error("Not 2xx response"); @@ -68,12 +68,12 @@ export default { } }) .catch((e) => console.log(e)); - this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, { + this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, this.fetchOptions({ headers: { Authorization: "Bearer " + this.item.apikey, Accept: "application/json", }, - }) + })) .then(function (response) { if (!response.ok) { throw new Error("Not 2xx response"); diff --git a/src/components/services/Medusa.vue b/src/components/services/Medusa.vue index 57206493c..1d445d942 100644 --- a/src/components/services/Medusa.vue +++ b/src/components/services/Medusa.vue @@ -71,10 +71,12 @@ export default { }, methods: { fetchConfig: function () { - fetch(`${this.item.url}/api/v2/config`, { - credentials: "include", - headers: { "X-Api-Key": `${this.item.apikey}` }, - }) + fetch( + `${this.item.url}/api/v2/config`, + this.fetchOptions({ + headers: { "X-Api-Key": `${this.item.apikey}` }, + }) + ) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); diff --git a/src/components/services/OpenWeather.vue b/src/components/services/OpenWeather.vue index 09ff76a3d..b43dca9c4 100644 --- a/src/components/services/OpenWeather.vue +++ b/src/components/services/OpenWeather.vue @@ -65,7 +65,7 @@ export default { } const url = `https://api.openweathermap.org/data/2.5/weather?${locationQuery}&appid=${this.item.apiKey}&units=${this.item.units}`; - fetch(url) + fetch(url, this.fetchOptions()) .then((response) => { if (!response.ok) { throw Error(response.statusText); diff --git a/src/components/services/PaperlessNG.vue b/src/components/services/PaperlessNG.vue index af1331785..8b7841e57 100644 --- a/src/components/services/PaperlessNG.vue +++ b/src/components/services/PaperlessNG.vue @@ -58,12 +58,11 @@ export default { return; } const url = `${this.item.url}/api/documents/`; - this.api = await fetch(url, { - credentials: "include", + this.api = await fetch(url, this.fetchOptions({ headers: { Authorization: "Token " + this.item.apikey, }, - }) + })) .then(function (response) { if (!response.ok) { throw new Error("Not 2xx response"); diff --git a/src/components/services/PiHole.vue b/src/components/services/PiHole.vue index 87f7090d4..2176cb742 100644 --- a/src/components/services/PiHole.vue +++ b/src/components/services/PiHole.vue @@ -64,9 +64,8 @@ export default { methods: { fetchStatus: async function () { const url = `${this.item.url}/api.php`; - this.api = await fetch(url, { - credentials: "include", - }) + const fetchOptions = this.fetchOptions(); + this.api = await fetch(url, fetchOptions) .then((response) => response.json()) .catch((e) => console.log(e)); }, diff --git a/src/components/services/Ping.vue b/src/components/services/Ping.vue index e693af4cc..c35c4fb4f 100644 --- a/src/components/services/Ping.vue +++ b/src/components/services/Ping.vue @@ -50,11 +50,10 @@ export default { methods: { fetchStatus: async function () { const url = `${this.item.url}`; - fetch(url, { + fetch(url, this.fetchOptions({ method: "HEAD", cache: "no-cache", - credentials: "include", - }) + })) .then((response) => { if (!response.ok) { throw Error(response.statusText); diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue index a9cdedf0d..b1efd19e4 100644 --- a/src/components/services/Radarr.vue +++ b/src/components/services/Radarr.vue @@ -70,9 +70,7 @@ export default { }, methods: { fetchConfig: function () { - fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { - credentials: "include", - }) + fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, this.fetchOptions()) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); @@ -94,9 +92,7 @@ export default { console.error(e); this.serverError = true; }); - fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { - credentials: "include", - }) + fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, this.fetchOptions()) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue index 027025599..27538b45c 100644 --- a/src/components/services/Sonarr.vue +++ b/src/components/services/Sonarr.vue @@ -70,9 +70,7 @@ export default { }, methods: { fetchConfig: function () { - fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, { - credentials: "include", - }) + fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, this.fetchOptions()) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); @@ -94,9 +92,7 @@ export default { console.error(e); this.serverError = true; }); - fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, { - credentials: "include", - }) + fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, this.fetchOptions()) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); diff --git a/src/main.js b/src/main.js index e5995a44e..7871ee430 100644 --- a/src/main.js +++ b/src/main.js @@ -14,6 +14,21 @@ Vue.component("DynamicStyle", { }, }); +Vue.mixin({ + methods: { + fetchOptions(options = {}) { + if ( + this.item && + this.item.fetchWithCredentials && + this.item.fetchWithCredentials == true + ) { + options.credentials = "include"; + } + return options; + }, + }, +}); + new Vue({ render: (h) => h(App), }).$mount("#app"); From 02a93f5290956d0b39a2e4aeb5171c6ada9af17e Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 30 Sep 2021 11:50:47 -0700 Subject: [PATCH 02/10] Lint --- src/assets/app.scss | 8 ++++--- src/components/services/Mealie.vue | 30 +++++++++++++++---------- src/components/services/PaperlessNG.vue | 13 ++++++----- src/components/services/Ping.vue | 11 +++++---- src/components/services/Radarr.vue | 10 +++++++-- src/components/services/Sonarr.vue | 10 +++++++-- 6 files changed, 54 insertions(+), 28 deletions(-) diff --git a/src/assets/app.scss b/src/assets/app.scss index f2dfb37e4..de0542e81 100644 --- a/src/assets/app.scss +++ b/src/assets/app.scss @@ -13,7 +13,9 @@ text-overflow: ellipsis; } -html, body, body #app { +html, +body, +body #app { height: 100%; background-color: var(--background); } @@ -152,8 +154,8 @@ body { background-color: var(--highlight-hover); } } - .navbar-menu { - background-color: inherit; + .navbar-menu { + background-color: inherit; } } .navbar-end { diff --git a/src/components/services/Mealie.vue b/src/components/services/Mealie.vue index 0c439bfa1..0a46b7075 100644 --- a/src/components/services/Mealie.vue +++ b/src/components/services/Mealie.vue @@ -52,12 +52,15 @@ export default { methods: { fetchStatus: async function () { if (this.item.subtitle != null) return; // omitting unnecessary ajax call as the subtitle is showing - this.meal = await fetch(`${this.item.url}/api/meal-plans/today/`, this.fetchOptions({ - headers: { - Authorization: "Bearer " + this.item.apikey, - Accept: "application/json", - }, - })) + this.meal = await fetch( + `${this.item.url}/api/meal-plans/today/`, + this.fetchOptions({ + headers: { + Authorization: "Bearer " + this.item.apikey, + Accept: "application/json", + }, + }) + ) .then(function (response) { if (!response.ok) { throw new Error("Not 2xx response"); @@ -68,12 +71,15 @@ export default { } }) .catch((e) => console.log(e)); - this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, this.fetchOptions({ - headers: { - Authorization: "Bearer " + this.item.apikey, - Accept: "application/json", - }, - })) + this.stats = await fetch( + `${this.item.url}/api/debug/statistics/`, + this.fetchOptions({ + headers: { + Authorization: "Bearer " + this.item.apikey, + Accept: "application/json", + }, + }) + ) .then(function (response) { if (!response.ok) { throw new Error("Not 2xx response"); diff --git a/src/components/services/PaperlessNG.vue b/src/components/services/PaperlessNG.vue index 8b7841e57..70b635be7 100644 --- a/src/components/services/PaperlessNG.vue +++ b/src/components/services/PaperlessNG.vue @@ -58,11 +58,14 @@ export default { return; } const url = `${this.item.url}/api/documents/`; - this.api = await fetch(url, this.fetchOptions({ - headers: { - Authorization: "Token " + this.item.apikey, - }, - })) + this.api = await fetch( + url, + this.fetchOptions({ + headers: { + Authorization: "Token " + this.item.apikey, + }, + }) + ) .then(function (response) { if (!response.ok) { throw new Error("Not 2xx response"); diff --git a/src/components/services/Ping.vue b/src/components/services/Ping.vue index c35c4fb4f..82188a5c8 100644 --- a/src/components/services/Ping.vue +++ b/src/components/services/Ping.vue @@ -50,10 +50,13 @@ export default { methods: { fetchStatus: async function () { const url = `${this.item.url}`; - fetch(url, this.fetchOptions({ - method: "HEAD", - cache: "no-cache", - })) + fetch( + url, + this.fetchOptions({ + method: "HEAD", + cache: "no-cache", + }) + ) .then((response) => { if (!response.ok) { throw Error(response.statusText); diff --git a/src/components/services/Radarr.vue b/src/components/services/Radarr.vue index b1efd19e4..abd189510 100644 --- a/src/components/services/Radarr.vue +++ b/src/components/services/Radarr.vue @@ -70,7 +70,10 @@ export default { }, methods: { fetchConfig: function () { - fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, this.fetchOptions()) + fetch( + `${this.item.url}/api/health?apikey=${this.item.apikey}`, + this.fetchOptions() + ) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); @@ -92,7 +95,10 @@ export default { console.error(e); this.serverError = true; }); - fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, this.fetchOptions()) + fetch( + `${this.item.url}/api/queue?apikey=${this.item.apikey}`, + this.fetchOptions() + ) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); diff --git a/src/components/services/Sonarr.vue b/src/components/services/Sonarr.vue index 27538b45c..e8442ca9b 100644 --- a/src/components/services/Sonarr.vue +++ b/src/components/services/Sonarr.vue @@ -70,7 +70,10 @@ export default { }, methods: { fetchConfig: function () { - fetch(`${this.item.url}/api/health?apikey=${this.item.apikey}`, this.fetchOptions()) + fetch( + `${this.item.url}/api/health?apikey=${this.item.apikey}`, + this.fetchOptions() + ) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); @@ -92,7 +95,10 @@ export default { console.error(e); this.serverError = true; }); - fetch(`${this.item.url}/api/queue?apikey=${this.item.apikey}`, this.fetchOptions()) + fetch( + `${this.item.url}/api/queue?apikey=${this.item.apikey}`, + this.fetchOptions() + ) .then((response) => { if (response.status != 200) { throw new Error(response.statusText); From 2293c8f8d3e9521d1bc01b347a2b6992633948a1 Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 30 Sep 2021 12:07:07 -0700 Subject: [PATCH 03/10] Refactor to import --- src/main.js | 16 ++-------------- src/mixins/fetchOptions.js | 14 ++++++++++++++ 2 files changed, 16 insertions(+), 14 deletions(-) create mode 100644 src/mixins/fetchOptions.js diff --git a/src/main.js b/src/main.js index 7871ee430..4b431922f 100644 --- a/src/main.js +++ b/src/main.js @@ -4,6 +4,7 @@ import "./registerServiceWorker"; import "@fortawesome/fontawesome-free/css/all.css"; +import fetchOptions from "./mixins/fetchOptions.js"; import "./assets/app.scss"; Vue.config.productionTip = false; @@ -14,20 +15,7 @@ Vue.component("DynamicStyle", { }, }); -Vue.mixin({ - methods: { - fetchOptions(options = {}) { - if ( - this.item && - this.item.fetchWithCredentials && - this.item.fetchWithCredentials == true - ) { - options.credentials = "include"; - } - return options; - }, - }, -}); +Vue.mixin(fetchOptions); new Vue({ render: (h) => h(App), diff --git a/src/mixins/fetchOptions.js b/src/mixins/fetchOptions.js new file mode 100644 index 000000000..5f162d501 --- /dev/null +++ b/src/mixins/fetchOptions.js @@ -0,0 +1,14 @@ +export default { + methods: { + fetchOptions(options = {}) { + if ( + this.item && + this.item.fetchWithCredentials && + this.item.fetchWithCredentials == true + ) { + options.credentials = "include"; + } + return options; + }, + }, +}; From 76ae089b4c8a781bfd846cf0a8e74301d11d728e Mon Sep 17 00:00:00 2001 From: Colin Date: Thu, 30 Sep 2021 12:18:25 -0700 Subject: [PATCH 04/10] Add documentation --- docs/configuration.md | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/configuration.md b/docs/configuration.md index d90981a43..f7f06d80e 100644 --- a/docs/configuration.md +++ b/docs/configuration.md @@ -131,6 +131,7 @@ services: tag: "other" url: "http://192.168.0.151/admin" type: "PiHole" # optional, loads a specific component that provides extra features. MUST MATCH a file name (without file extension) available in `src/components/services` + # fetchWithCredentials: true # optional adds cookies to fetch calls for external data. Useful for services running behind SSO clients e.g. Authelia target: "_blank" # optional html a tag target attribute # class: "green" # optional custom CSS class for card, useful with custom stylesheet # background: red # optional color for card to set color directly without custom stylesheet From d50602701ec593405e190f53c49fd28e7e263ae4 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 1 Oct 2021 10:37:10 -0700 Subject: [PATCH 05/10] More documentation --- docs/development.md | 24 ++++++++++++++++++++++++ docs/troubleshooting.md | 17 ++++++++++++++++- 2 files changed, 40 insertions(+), 1 deletion(-) diff --git a/docs/development.md b/docs/development.md index 5e432f1de..a84ed92d8 100644 --- a/docs/development.md +++ b/docs/development.md @@ -27,3 +27,27 @@ body #app.theme-my-awesome-theme. { ... } ... @import "./themes/my-awesome-theme.scss"; ``` + +## Fetch Options + +In order to make your service work with the global `fetchWithCredentials` attribute, you need to include a call to `this.fetchOptions()` as the optional second parameter of your `fetch` call. This allows us to hook in and add global options for all fetch calls as needed. + +`fetchOptions()` itself takes an optional object just like the usual `fetch` param, and would conditionally add other options as needed. + +### Basic example +```js +fetch( + `${this.item.url}/api/health?apikey=${this.item.apikey}`, + this.fetchOptions() +) +``` + +### Example with other fetch options +```js +fetch( + `${this.item.url}/api/v2/config`, + this.fetchOptions({ + headers: { "X-Api-Key": `${this.item.apikey}` }, + }) +) +``` diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index ed1f85d58..7d8f0f352 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,4 +1,4 @@ -# Troubleshooting +ong# Troubleshooting ## My custom service card doesn't work, nothing appears or offline status is displayed (pi-hole, sonarr, ping, ...) @@ -17,3 +17,18 @@ To resolve this, you can either: * Host all your target service under the same domain & port. * Modify the target sever configuration so that the response of the server included following header- `Access-Control-Allow-Origin: *` (). It might be an option in the targeted service, otherwise depending on how the service is hosted, the proxy or web server can seamlessly add it. * Use a cors proxy sever like [`cors-container`](https://github.com/imjacobclark/cors-container), [`cors-anywhere`](https://github.com/Rob--W/cors-anywhere) or many others. + + +## I use a single sign on tool, like Authelia. How do I get the service calls to not hit the login page? + +All the services use the JS `fetch` function. By default, a `fetch` call doesn't include the browser cookies that store your SSO session credentials. If you want to include cookie info in the API calls, you can add the global `fetchWithCredentials` attribute to your service item. Here's an example below for PiHole. + +```yml +- name: "Pi-Hole" + logo: "assets/icons/pihole.svg" + url: "https://pihole.mydomain.com/admin" + type: "PiHole" + fetchWithCredentials: true +``` + +> **Note for developers:** Read the information in `docs/development.md` for how to make sure your service template works with `fetchWithCredentials` From 7ec0990b7b16a89b64ab8b57bc7edaa8ab91ac39 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 1 Oct 2021 10:46:30 -0700 Subject: [PATCH 06/10] Fix js sourcemaps in Chrome devtools --- vue.config.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/vue.config.js b/vue.config.js index 410acc89c..22a5c511a 100644 --- a/vue.config.js +++ b/vue.config.js @@ -9,6 +9,9 @@ module.exports = { .loader("raw-loader") .end(); }, + configureWebpack: { + devtool: "source-map", + }, publicPath: "", pwa: { manifestPath: "assets/manifest.json", From 0ddff5d99cce08d10bd52ff1d4d4056ded58de57 Mon Sep 17 00:00:00 2001 From: Colin Date: Fri, 1 Oct 2021 10:57:22 -0700 Subject: [PATCH 07/10] Remove typo --- docs/troubleshooting.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 7d8f0f352..33c9a8ece 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -1,4 +1,4 @@ -ong# Troubleshooting +# Troubleshooting ## My custom service card doesn't work, nothing appears or offline status is displayed (pi-hole, sonarr, ping, ...) From 1834f6ae0f84d12ca6144a157dd8fa3e8f2ab77c Mon Sep 17 00:00:00 2001 From: Colin Date: Wed, 3 Nov 2021 13:48:36 -0700 Subject: [PATCH 08/10] convert fetchOptions to service.fetch --- src/components/services/AdGuardHome.vue | 2 -- src/components/services/Mealie.vue | 30 ++++++++++--------------- src/components/services/Medusa.vue | 9 +++----- src/components/services/OpenWeather.vue | 2 +- src/components/services/PaperlessNG.vue | 13 +++++------ src/components/services/PiHole.vue | 3 +-- src/components/services/Ping.vue | 2 -- src/components/services/Prometheus.vue | 2 -- src/components/services/Radarr.vue | 10 ++------- src/components/services/Sonarr.vue | 10 ++------- src/main.js | 4 ++-- src/mixins/fetchOptions.js | 14 ------------ src/mixins/service.js | 25 +++------------------ 13 files changed, 31 insertions(+), 95 deletions(-) delete mode 100644 src/mixins/fetchOptions.js diff --git a/src/components/services/AdGuardHome.vue b/src/components/services/AdGuardHome.vue index b01f0f49a..9a6eb1db9 100644 --- a/src/components/services/AdGuardHome.vue +++ b/src/components/services/AdGuardHome.vue @@ -20,12 +20,10 @@