Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Optional fetch with credentials #301

Closed
Closed
Show file tree
Hide file tree
Changes from 7 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
24 changes: 24 additions & 0 deletions docs/development.md
Original file line number Diff line number Diff line change
Expand Up @@ -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}` },
})
)
```
15 changes: 15 additions & 0 deletions docs/troubleshooting.md
Original file line number Diff line number Diff line change
Expand Up @@ -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: *` (<https://developer.mozilla.org/en-US/docs/Web/HTTP/CORS#simple_requests>). 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`
8 changes: 5 additions & 3 deletions src/assets/app.scss
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,9 @@
text-overflow: ellipsis;
}

html, body, body #app {
html,
body,
body #app {
height: 100%;
background-color: var(--background);
}
Expand Down Expand Up @@ -152,8 +154,8 @@ body {
background-color: var(--highlight-hover);
}
}
.navbar-menu {
background-color: inherit;
.navbar-menu {
background-color: inherit;
}
}
.navbar-end {
Expand Down
14 changes: 8 additions & 6 deletions src/components/services/AdGuardHome.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
Expand Down
30 changes: 18 additions & 12 deletions src/components/services/Mealie.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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/`, {
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");
Expand All @@ -68,12 +71,15 @@ export default {
}
})
.catch((e) => console.log(e));
this.stats = await fetch(`${this.item.url}/api/debug/statistics/`, {
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");
Expand Down
10 changes: 6 additions & 4 deletions src/components/services/Medusa.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion src/components/services/OpenWeather.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
14 changes: 8 additions & 6 deletions src/components/services/PaperlessNG.vue
Original file line number Diff line number Diff line change
Expand Up @@ -58,12 +58,14 @@ export default {
return;
}
const url = `${this.item.url}/api/documents/`;
this.api = await fetch(url, {
credentials: "include",
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");
Expand Down
5 changes: 2 additions & 3 deletions src/components/services/PiHole.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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));
},
Expand Down
12 changes: 7 additions & 5 deletions src/components/services/Ping.vue
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,13 @@ export default {
methods: {
fetchStatus: async function () {
const url = `${this.item.url}`;
fetch(url, {
method: "HEAD",
cache: "no-cache",
credentials: "include",
})
fetch(
url,
this.fetchOptions({
method: "HEAD",
cache: "no-cache",
})
)
.then((response) => {
if (!response.ok) {
throw Error(response.statusText);
Expand Down
14 changes: 8 additions & 6 deletions src/components/services/Radarr.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ 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);
Expand All @@ -94,9 +95,10 @@ 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);
Expand Down
14 changes: 8 additions & 6 deletions src/components/services/Sonarr.vue
Original file line number Diff line number Diff line change
Expand Up @@ -70,9 +70,10 @@ 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);
Expand All @@ -94,9 +95,10 @@ 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);
Expand Down
3 changes: 3 additions & 0 deletions src/main.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -14,6 +15,8 @@ Vue.component("DynamicStyle", {
},
});

Vue.mixin(fetchOptions);

new Vue({
render: (h) => h(App),
}).$mount("#app");
14 changes: 14 additions & 0 deletions src/mixins/fetchOptions.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
export default {
methods: {
fetchOptions(options = {}) {
if (
this.item &&
this.item.fetchWithCredentials &&
this.item.fetchWithCredentials == true
) {
options.credentials = "include";
}
return options;
},
},
};
3 changes: 3 additions & 0 deletions vue.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@ module.exports = {
.loader("raw-loader")
.end();
},
configureWebpack: {
devtool: "source-map",
},
publicPath: "",
pwa: {
manifestPath: "assets/manifest.json",
Expand Down