diff --git a/.env b/.env index c79532129c..612a0937af 100644 --- a/.env +++ b/.env @@ -1,40 +1,59 @@ -# Store environmental variables here. All variables are optional. -# Lines beginning in '#' are ignored. - -# Can be either development, production or test -# NODE_ENV=production - -# The port to expose the running application on -# PORT=4000 - -# If you've proved SSL certs, then can set HTTPS port -# SSL_PORT=4001 - -# The host that Dashy is running on, domain or IP -# HOST=localhost - -# The default base path for serving up static assets -# BASE_URL=./ - -# Optionally, specify the path of SSL private + public keys -# SSL_PRIV_KEY_PATH=/etc/ssl/certs/dashy-priv.key -# SSL_PUB_KEY_PATH=/etc/ssl/certs/dashy-pub.pem - -# If SSL enabled, choose whether or not to redirect http to https -# Defaults to true -# REDIRECT_HTTPS=true - -# Usually the same as BASE_URL, but accessible in frontend -# VUE_APP_DOMAIN=https://dashy.to - -# Should enable SRI for build script and link resources -# INTEGRITY=true - -# Computed automatically on build. Indicates if running in container -# IS_DOCKER=true - -# Again, set automatically using package.json during build time -# VUE_APP_VERSION=2.0.0 - -# Directory for conf.yml backups -# BACKUP_DIR=./public/ \ No newline at end of file +# Store environmental variables here. All variables are optional. +# Lines beginning in '#' are ignored. + +# Can be either development, production or test +# NODE_ENV=production + +# The port to expose the running application on +# PORT=4000 + +# If you've proved SSL certs, then can set HTTPS port +# SSL_PORT=4001 + +# The host that Dashy is running on, domain or IP +# HOST=localhost + +# The default base path for serving up static assets +# BASE_URL=./ + +# Optionally, specify the path of SSL private + public keys +# SSL_PRIV_KEY_PATH=/etc/ssl/certs/dashy-priv.key +# SSL_PUB_KEY_PATH=/etc/ssl/certs/dashy-pub.pem + +# If SSL enabled, choose whether or not to redirect http to https +# Defaults to true +# REDIRECT_HTTPS=true + +# The path to the user data directory +# USER_DATA_DIR=user-data + +# Override where the path to the configuration file is, can be a remote URL +# VUE_APP_CONFIG_PATH=/conf.yml + +# Usually the same as BASE_URL, but accessible in frontend +# VUE_APP_DOMAIN=https://dashy.to + +# Override the page title for the frontend app +# VUE_APP_TITLE='' + +# Set the default view to load on startup (can be `minimal`, `workspace` or `home`) +# VUE_APP_STARTING_VIEW=home + +# Set the Vue app routing mode (can be 'hash', 'history' or 'abstract') +# VUE_APP_ROUTING_MODE=history + +# Should enable SRI for build script and link resources +# INTEGRITY=true + +# Computed automatically on build. Indicates if running in container +# IS_DOCKER=true + +# Again, set automatically using package.json during build time +# VUE_APP_VERSION=2.0.0 + +# Directory for conf.yml backups +# BACKUP_DIR=./user-data/ + +# Setup any other user defined vars by prepending VUE_APP_ to the var name +# VUE_APP_pihole_ip=http://your.pihole.ip +# VUE_APP_pihole_key=your_pihole_secret_key diff --git a/.github/AUTHORS.txt b/.github/AUTHORS.txt index 116860e770..3b1440ba9e 100644 --- a/.github/AUTHORS.txt +++ b/.github/AUTHORS.txt @@ -163,5 +163,5 @@ Lissy93 - 222 commits Alicia Bot <87835202+liss-bot@users.noreply.github.com> - 240 commits liss-bot - 244 commits Alicia Sykes - 439 commits -Alicia Sykes - 505 commits -Alicia Sykes - 1488 commits \ No newline at end of file +Alicia Sykes - 471 commits +Alicia Sykes - 1488 commits diff --git a/.github/workflows/auto-tag-pr.yml b/.github/workflows/auto-tag-pr.yml index ce4cde10d6..d3a575bdd1 100644 --- a/.github/workflows/auto-tag-pr.yml +++ b/.github/workflows/auto-tag-pr.yml @@ -34,6 +34,7 @@ jobs: bodyFile: ".github/LATEST_CHANGELOG.md" mark-issue-fixed: runs-on: ubuntu-latest + if: ${{ github.event_name == 'issues' }} steps: - uses: actions/checkout@v2 - name: Label Fixed Issues diff --git a/.github/workflows/broadcast-message.yml b/.github/workflows/broadcast-message.yml deleted file mode 100644 index da0ece177a..0000000000 --- a/.github/workflows/broadcast-message.yml +++ /dev/null @@ -1,14 +0,0 @@ -# Updates multiple issues with a certain tag, with a comment containing a given message -name: đŸŽ¯ Broadcast Message across Issues -on: - workflow_dispatch: - inputs: - message: { required: false } - labels: { required: false } -jobs: - broadcast: - runs-on: ubuntu-latest - steps: - - uses: jenschelkopf/broadcast-action@master - with: - token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} \ No newline at end of file diff --git a/.github/workflows/dependency-updates-summary.yml b/.github/workflows/dependency-updates-summary.yml index 24fedb2de9..a9c4130345 100644 --- a/.github/workflows/dependency-updates-summary.yml +++ b/.github/workflows/dependency-updates-summary.yml @@ -15,4 +15,3 @@ jobs: collapsibleThreshold: '25' failOnDowngrade: 'false' path: 'yarn.lock' - updateComment: 'true' diff --git a/.github/workflows/generate-credits.yml b/.github/workflows/generate-credits.yml index 4746b732bd..0ec6a20813 100644 --- a/.github/workflows/generate-credits.yml +++ b/.github/workflows/generate-credits.yml @@ -66,18 +66,3 @@ jobs: committer_username: liss-bot committer_email: liss-bot@d0h.co - make-author-list: - runs-on: ubuntu-latest - steps: - - name: Checkout 🛎ī¸ - uses: actions/checkout@v2 - with: - fetch-depth: 0 - - uses: wow-actions/update-authors@v1.1.4 - with: - GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - sort: commits - bots: true - path: .github/AUTHORS.txt - commit: ':blue_heart: Makes author list' - template: '{{name}} <{{email}}> - {{commits}} commits' diff --git a/.github/workflows/release-commenter.yml b/.github/workflows/release-commenter.yml index 4f2ab18700..eff58e1c8f 100644 --- a/.github/workflows/release-commenter.yml +++ b/.github/workflows/release-commenter.yml @@ -10,10 +10,8 @@ jobs: - uses: apexskier/github-release-commenter@v1 with: GITHUB_TOKEN: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }} - label-template: 🛩ī¸ Released {release_tag}, 🔨 Fixed + label-template: 🛩ī¸ Released {release_tag} comment-template: | - **The fix for this issue has now been released in {release_name} ✨** + **This has now been released in {release_name} ✨** If you haven't done so already, please [update your instance](https://github.com/Lissy93/dashy/blob/master/docs/management.md#updating) to `{release_tag}` or later. See {release_link} for full info. - - Feel free to reach out if you need any more support. If you are enjoying Dashy, consider [supporting the project](https://github.com/Lissy93/dashy/blob/master/docs/contributing.md#contributing). diff --git a/.yarnrc.yml b/.yarnrc.yml new file mode 100644 index 0000000000..3186f3f079 --- /dev/null +++ b/.yarnrc.yml @@ -0,0 +1 @@ +nodeLinker: node-modules diff --git a/Dockerfile b/Dockerfile index f04224f9fa..cba3f347fb 100644 --- a/Dockerfile +++ b/Dockerfile @@ -28,7 +28,7 @@ RUN yarn build --mode production FROM node:20.11.1-alpine3.19 # Define some ENV Vars -ENV PORT=80 \ +ENV PORT=8080 \ DIRECTORY=/app \ IS_DOCKER=true @@ -40,8 +40,6 @@ RUN apk add --no-cache tzdata # Copy built application from build phase COPY --from=BUILD_IMAGE /app ./ -# Ensure only one version of conf.yml exists -RUN rm dist/conf.yml # Finally, run start command to serve up the built application CMD [ "yarn", "build-and-start" ] diff --git a/README.md b/README.md index 4ea727da60..6690cd3cf4 100644 --- a/README.md +++ b/README.md @@ -6,27 +6,12 @@
User Showcase | Live Demo | Getting Started | Documentation | GitHub -

- - Awesome Self-Hosted - - - License MIT - - - Current Version - - - Docker Pulls - - - GitHub Status - - - Known Vulnerabilities -

+> [!NOTE] +> Version [3.0.0](https://github.com/Lissy93/dashy/releases/tag/3.0.0) has been released, and requires some changes to your setup, see [#1529](https://github.com/Lissy93/dashy/discussions/1529) for details. + +
Table of Contents

@@ -95,7 +80,7 @@ **Screenshots**: Checkout the [Showcase](./docs/showcase.md), to see example dashboards from the community -**Spin up your own demo**: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml) or [`docker run -p 8080:80 lissy93/dashy`](./docs/quick-start.md) +**Spin up your own demo**: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml) or [`docker run -p 8080:8080 lissy93/dashy`](./docs/quick-start.md)

@@ -116,15 +101,15 @@ You will need [Docker](https://docs.docker.com/get-docker/) installed on your system ``` -docker run -p 8080:80 lissy93/dashy +docker run -p 8080:8080 lissy93/dashy ``` Or ```docker docker run -d \ - -p 4000:80 \ - -v /root/my-local-conf.yml:/app/public/conf.yml \ + -p 4000:8080 \ + -v /root/my-local-conf.yml:/app/user-data/conf.yml \ --name my-dashboard \ --restart=always \ lissy93/dashy:latest @@ -140,7 +125,7 @@ See also: [examples with Docker Compose](./docs/deployment.md#using-docker-compo You will need [git](https://git-scm.com/downloads), the latest or LTS version of [Node.js](https://nodejs.org/) and _(optionally)_ [Yarn](https://yarnpkg.com/) installed on your system. - Clone the Repo: `git clone https://github.com/Lissy93/dashy.git` and `cd dashy` -- Configuration: Fill in your settings in `./public/conf.yml` +- Configuration: Fill in your settings in `./user-data/conf.yml` - Install dependencies: `yarn` - Build: `yarn build` - Run: `yarn start` @@ -169,7 +154,7 @@ Dashy supports **1-Click deployments** on several popular cloud platforms. To sp > For full configuration documentation, see: [**Configuring**](./docs/configuring.md) -Dashy is configured through a YAML file, located at `./public/conf.yml`. In addition, you can find a complete list of available options in the [Configuring Docs](./docs/configuring.md). The config can also be edited and saved directly through the UI. +Dashy is configured through a YAML file, located at `./user-data/conf.yml`. In addition, you can find a complete list of available options in the [Configuring Docs](./docs/configuring.md). The config can also be edited and saved directly through the UI. **[âŦ†ī¸ Back to Top](#dashy)** @@ -581,7 +566,8 @@ Huge thanks to the sponsors helping to support Dashy's development!
Shrippen - + + bile0026 @@ -843,16 +829,25 @@ For more info, see TLDR Legal's [Explanation of MIT](https://tldrlegal.com/licen --- - -

-
-
- - -

- - - -

- Thank you for Visiting + +

+ Š Alicia Sykes 2024
+ Licensed under MIT
+
+ Thanks for visiting :)

+ + + + diff --git a/docker-compose.yml b/docker-compose.yml index 0d09a26abb..9eb391eb03 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -12,21 +12,17 @@ services: # To build from source, replace 'image: lissy93/dashy' with 'build: .' # build: . - # Or, to use a Dockerfile for your archtecture, uncomment the following - # context: . - # dockerfile: ./docker/Dockerfile-arm32v7 - # You can also use an image with a different tag, or pull from a different registry, e.g: - # image: ghcr.io/lissy93/dashy or image: lissy93/dashy:arm64v8 + # image: ghcr.io/lissy93/dashy or image: lissy93/dashy:3.0.0 # Pass in your config file below, by specifying the path on your host machine # volumes: - # - /path/to/my-config.yml:/app/public/conf.yml - # - /path/to/item-icons:/app/public/item-icons + # - /path/to/my-config.yml:/app/user-data/conf.yml + # - /path/to/item-icons:/app/user-data/item-icons/ # Set port that web service will be served on. Keep container port as 80 ports: - - 4000:80 + - 4000:8080 # Set any environmental variables environment: diff --git a/docker/docker-readme.md b/docker/docker-readme.md index 147211e70b..af447bf43d 100644 --- a/docker/docker-readme.md +++ b/docker/docker-readme.md @@ -55,7 +55,7 @@ **Screenshots**: Checkout the [Showcase](https://github.com/Lissy93/dashy/blob/master/docs/showcase.md), to see example dashboards from the community -**Spin up your own demo**: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml) or [`docker run -p 8080:80 lissy93/dashy`](./docs/quick-start.md) +**Spin up your own demo**: [![One-Click Deploy with PWD](https://img.shields.io/badge/Play--with--Docker-Deploy-2496ed?style=flat-square&logo=docker)](https://labs.play-with-docker.com/?stack=https://raw.githubusercontent.com/Lissy93/dashy/master/docker-compose.yml) or [`docker run -p 8080:8080 lissy93/dashy`](./docs/quick-start.md)

@@ -69,7 +69,7 @@ ## Getting Started đŸ›Ģ -To deploy Dashy with Docker, just run `docker run -p 8080:80 lissy93/dashy`, then open `http://localhost:8080` +To deploy Dashy with Docker, just run `docker run -p 8080:8080 lissy93/dashy`, then open `http://localhost:8080` For full list of options and a Docker compose file, see the [Deployment Docs](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md). diff --git a/docs/assets/CONTRIBUTORS.svg b/docs/assets/CONTRIBUTORS.svg index b881fbf8b7..74cffd0a3d 100644 --- a/docs/assets/CONTRIBUTORS.svg +++ b/docs/assets/CONTRIBUTORS.svg @@ -363,4 +363,4 @@ - \ No newline at end of file + diff --git a/docs/assets/repo-visualization.svg b/docs/assets/repo-visualization.svg index 44005e32da..d4c2b13b11 100644 --- a/docs/assets/repo-visualization.svg +++ b/docs/assets/repo-visualization.svg @@ -1 +1 @@ -viewsviewsutilsutilsstylesstylesmixinsmixinscomponentscomponentsassetsassetsWorkspaceWorkspaceWidgetsWidgetsSettingsSettingsPageStrcturePageStrctureMinimalViewMinimalViewLinkItemsLinkItemsInteractiveEditorInteractiveEditorFormElementsFormElementsConfigurationConfigurationChartsChartslocaleslocalesinterface-iconsinterface-icons.js.json.scss.svg.vueeach dot sized by file size \ No newline at end of file +viewsviewsutilsutilsstylesstylesmixinsmixinscomponentscomponentsassetsassetsWorkspaceWorkspaceWidgetsWidgetsSettingsSettingsPageStrcturePageStrctureMinimalViewMinimalViewLinkItemsLinkItemsInteractiveEditorInteractiveEditorFormElementsFormElementsConfigurationConfigurationChartsChartslocaleslocalesinterface-iconsinterface-icons.js.json.scss.svg.vueeach dot sized by file size diff --git a/docs/authentication.md b/docs/authentication.md index 694dbe9dd1..c7e74f8a77 100644 --- a/docs/authentication.md +++ b/docs/authentication.md @@ -20,7 +20,7 @@ > [!IMPORTANT] -> Dashy's built-in auth is not indented to protect a publicly hosted instance against unauthorized access. Instead you should use an auth provider compatible with your reverse proxy, or access Dashy via your VPN. +> Dashy's built-in auth is not indented to protect a publicly hosted instance against unauthorized access. Instead you should use an auth provider compatible with your reverse proxy, or access Dashy via your VPN, or implement your own SSO logic. > > In cases where Dashy is only accessibly within your home network, and you just want to add a login page, then the built-in auth may be sufficient, but keep in mind that configuration can still be accessed. @@ -28,6 +28,11 @@ Dashy has a basic login page included, and frontend authentication. You can enable this by adding users to the `auth` section under `appConfig` in your `conf.yml`. If this section is not specified, then no authentication will be required to access the app, and the homepage will resolve to your dashboard. +> [!NOTE] +> Since the auth is initiated in the main app entry point (for security), a rebuild is required to apply changes to the auth configuration. +> You can trigger a rebuild through the UI, under Config --> Rebuild, or by running `yarn build` in the root directory. + + ### Setting Up Authentication The `auth` property takes an array of users. Each user needs to include a username, hash and optional user type (`admin` or `normal`). The hash property is a [SHA-256 Hash](https://en.wikipedia.org/wiki/SHA-2) of your desired password. @@ -263,7 +268,7 @@ In NGINX you can specify [control access](https://docs.nginx.com/nginx/admin-gui ```text server { - listen 80; + listen 8080; server_name www.dashy.example.com; location / { root /path/to/dashy/; diff --git a/docs/backup-restore.md b/docs/backup-restore.md index 856b656455..f42e95e463 100644 --- a/docs/backup-restore.md +++ b/docs/backup-restore.md @@ -1,5 +1,7 @@ # Cloud Backup and Restore +Beyond the cloud backup/restore service, there are several other self-hosted options you can use to backup Dashy, and any other Docker container data. These are outlined in the Management docs, at: [Docker Backup Options](/docs/management.md#backing-up). + Dashy has a built-in feature for securely backing up your config to a hosted cloud service, and then restoring it on another instance. This feature is totally optional, and if you do not enable it, then Dashy will not make any external network requests. This is useful not only for backing up your configuration off-site, but it also enables Dashy to be used without having write a YAML config file, and makes it possible to use a public hosted instance, without the need to self-host. diff --git a/docs/configuring.md b/docs/configuring.md index 72558e70c2..3bcb0d4acd 100644 --- a/docs/configuring.md +++ b/docs/configuring.md @@ -1,6 +1,6 @@ # Configuring -All app configuration is specified in [`/public/conf.yml`](https://github.com/Lissy93/dashy/blob/master/public/conf.yml) which is in [YAML Format](https://yaml.org/) format. If you're using Docker, this file can be passed in as a volume. Changes can either be made directly to this file, or done [through the UI](#editing-config-through-the-ui). From the UI you can also export, backup, reset, validate and download your configuration file. +All app configuration is specified in [`/user-data/conf.yml`](https://github.com/Lissy93/dashy/blob/master/user-data/conf.yml) which is in [YAML Format](https://yaml.org/) format. If you're using Docker, this file can be passed in as a volume. Changes can either be made directly to this file, or done [through the UI](#editing-config-through-the-ui). From the UI you can also export, backup, reset, validate and download your configuration file. ## There are three ways to edit the config @@ -36,6 +36,7 @@ The following file provides a reference of all supported configuration options. - [`auth`](#appconfigauth-optional) - Built-in authentication setup - [`users`](#appconfigauthusers-optional) - List or users (for simple auth) - [`keycloak`](#appconfigauthkeycloak-optional) - Auth config for Keycloak + - [`headerAuth`](#appconfigauthheaderauth-optional) - Auth config for HeaderAuth - [**`sections`**](#section) - List of sections - [`displayData`](#sectiondisplaydata-optional) - Section display settings - [`show/hideForKeycloakUsers`](#sectiondisplaydatahideforkeycloakusers-sectiondisplaydatashowforkeycloakusers-itemdisplaydatahideforkeycloakusers-and-itemdisplaydatashowforkeycloakusers) - Set user controls @@ -101,7 +102,7 @@ The following file provides a reference of all supported configuration options. **Field** | **Type** | **Required**| **Description** --- | --- | --- | --- **`language`** | `string` | _Optional_ | The 2 (or 4-digit) [ISO 639-1 code](https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes) for your language, e.g. `en` or `en-GB`. This must be a language that the app has already been [translated](https://github.com/Lissy93/dashy/tree/master/src/assets/locales) into. If your language is unavailable, Dashy will fallback to English. By default Dashy will attempt to auto-detect your language, although this may not work on some privacy browsers. -**`startingView`** | `enum` | _Optional_ | Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI. Can be either `default`, `minimal` or `workspace`. Defaults to `default` +~~**`startingView`**~~ | `enum` | _Optional_ | Which page to load by default, and on the base page or domain root. You can still switch to different views from within the UI. Can be either `default`, `minimal` or `workspace`. Defaults to `default`. NOTE: This has been replaced by an environmental variable: `VUE_APP_STARTING_VIEW` in V3 onwards **`defaultOpeningMethod`** | `enum` | _Optional_ | The default opening method for items, if no `target` is specified for a given item. Can be either `newtab`, `sametab`, `modal`, `workspace`, `clipboard`, `top` or `parent`. Defaults to `newtab` **`statusCheck`** | `boolean` | _Optional_ | When set to `true`, Dashy will ping each of your services and display their status as a dot next to each item. This can be overridden by setting `statusCheck` under each item. Defaults to `false` **`statusCheckInterval`** | `number` | _Optional_ | The number of seconds between checks. If set to `0` then service will only be checked on initial page load, which is usually the desired functionality. If value is less than `10` you may experience a hit in performance. Defaults to `0` @@ -142,11 +143,21 @@ The following file provides a reference of all supported configuration options. ## `appConfig.auth` _(optional)_ +> [!NOTE] +> Since the auth is initiated in the main app entry point (for security), a rebuild is required to apply changes to the auth configuration. +> You can trigger a rebuild through the UI, under Config --> Rebuild, or by running `yarn build` in the root directory. + +> [!WARNING] +> Built-in auth should **not be used** for security-critical applications, or if your Dashy instance is publicly accessible. +> For these, it is recommended to use an [alternate authentication method](/docs/authentication.md#alternative-authentication-methods). + **Field** | **Type** | **Required**| **Description** --- | --- | --- | --- **`users`** | `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. See [`appConfig.auth.users`](#appconfigauthusers-optional).
**Note** this method of authentication is handled on the client side, so for security critical situations, it is recommended to use an [alternate authentication method](/docs/authentication.md#alternative-authentication-methods). **`enableKeycloak`** | `boolean` | _Optional_ | If set to `true`, then authentication using Keycloak will be enabled. Note that you need to have an instance running, and have also configured `auth.keycloak`. Defaults to `false` **`keycloak`** | `object` | _Optional_ | Config options to point Dashy to your Keycloak server. Requires `enableKeycloak: true`. See [`auth.keycloak`](#appconfigauthkeycloak-optional) for more info +**`enableHeaderAuth`** | `boolean` | _Optional_ | If set to `true`, then authentication using HeaderAuth will be enabled. Note that you need to have your web server/reverse proxy running, and have also configured `auth.headerAuth`. Defaults to `false` +**`headerAuth`** | `object` | _Optional_ | Config options to point Dashy to your headers for authentication. Requires `enableHeaderAuth: true`. See [`auth.headerAuth`](#appconfigauthheaderauth-optional) for more info **`enableGuestAccess`** | `boolean` | _Optional_ | When set to `true`, an unauthenticated user will be able to access the dashboard, with read-only access, without having to login. Requires `auth.users` to be configured. Defaults to `false`. For more info, see the **[Authentication Docs](/docs/authentication.md)** @@ -174,6 +185,15 @@ For more info, see the **[Authentication Docs](/docs/authentication.md)** **[âŦ†ī¸ Back to Top](#configuring)** +## `appConfig.auth.headerAuth` _(optional)_ + +**Field** | **Type** | **Required**| **Description** +--- | --- | --- | --- +**`userHeader`** | `string` | _Optional_ | The Header name which contains username (default: REMOTE_USER). Case insensitive +**`proxyWhitelist`** | `array` | Required | An array of Upstream proxy servers to expect authencticated requests from + +**[âŦ†ī¸ Back to Top](#configuring)** + ## `appConfig.webSearch` _(optional)_ **Field** | **Type** | **Required**| **Description** diff --git a/docs/deployment.md b/docs/deployment.md index 175a44b09b..21d6fdfda3 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -7,7 +7,7 @@ Welcome to Dashy, so glad you're here :) Deployment is super easy, and there are If you want to skip the fuss, and [get straight down to it](/docs/quick-start.md), then you can spin up a new instance of Dashy by running: ```bash -docker run -p 8080:80 lissy93/dashy +docker run -p 8080:8080 lissy93/dashy ``` See [Management Docs](/docs/management.md) for info about securing, monitoring, updating, health checks, auto starting, web server configuration, etc @@ -67,8 +67,8 @@ Dashy has a built container image hosted on [Docker Hub](https://hub.docker.com/ ```bash docker run -d \ - -p 8080:80 \ - -v /root/my-local-conf.yml:/app/public/conf.yml \ + -p 8080:8080 \ + -v /root/my-local-conf.yml:/app/user-data/conf.yml \ --name my-dashboard \ --restart=always \ lissy93/dashy:latest @@ -110,9 +110,9 @@ services: container_name: Dashy # Pass in your config file below, by specifying the path on your host machine # volumes: - # - /root/my-config.yml:/app/public/conf.yml + # - /root/my-config.yml:/app/user-data/conf.yml ports: - - 4000:80 + - 4000:8080 # Set any environmental variables environment: - NODE_ENV=production @@ -166,8 +166,8 @@ Installing dashy is really simply and fast: ```bash docker run -d \ - -p 4000:80 \ - -v /volume1/docker/dashy/my-local-conf.yml:/app/public/conf.yml \ + -p 4000:8080 \ + -v /volume1/docker/dashy/my-local-conf.yml:/app/user-data/conf.yml \ --name dashy \ --restart=always \ lissy93/dashy:latest @@ -182,7 +182,7 @@ dashy should be up within 1-2min after you've started the install task procedure If you do not want to use Docker, you can run Dashy directly on your host system. For this, you will need both [git](https://git-scm.com/downloads) and the latest or LTS version of [Node.js](https://nodejs.org/) installed, and optionally [yarn](https://yarnpkg.com/) 1. Get Code: `git clone https://github.com/Lissy93/dashy.git` and `cd dashy` -2. Configuration: Fill in you're settings in `./public/conf.yml` +2. Configuration: Fill in you're settings in `./user-data/conf.yml` 3. Install dependencies: `yarn` 4. Build: `yarn build` 5. Run: `yarn start` diff --git a/docs/developing.md b/docs/developing.md index 281f86f950..0168fc78a2 100644 --- a/docs/developing.md +++ b/docs/developing.md @@ -51,7 +51,7 @@ Dashy should now be being served on . Hot reload is enab #### Utils and Checks -- **`yarn validate-config`** - If you have quite a long configuration file, you may wish to check that it's all good to go, before deploying the app. This can be done with `yarn validate-config` or `docker exec -it [container-id] yarn validate-config`. Your config file needs to be in `/public/conf.yml` (or within your Docker container at `/app/public/conf.yml`). This will first check that your YAML is valid, and then validates it against Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js). +- **`yarn validate-config`** - If you have quite a long configuration file, you may wish to check that it's all good to go, before deploying the app. This can be done with `yarn validate-config` or `docker exec -it [container-id] yarn validate-config`. Your config file needs to be in `/user-data/conf.yml` (or within your Docker container at `/app/user-data/conf.yml`). This will first check that your YAML is valid, and then validates it against Dashy's [schema](https://github.com/Lissy93/dashy/blob/master/src/utils/ConfigSchema.js). - **`yarn health-check`** - Checks that the application is up and running on it's specified port, and outputs current status and response times. Useful for integrating into your monitoring service, if you need to maintain high system availability #### Alternate Start Commands diff --git a/docs/development-guides.md b/docs/development-guides.md index 54d854d6a8..ffc6bd75e3 100644 --- a/docs/development-guides.md +++ b/docs/development-guides.md @@ -104,7 +104,7 @@ If you are not comfortable with making pull requests, or do not want to modify t This section is for, adding a new setting to the config file. -All of the users config is specified in `./public/conf.yml` - see [Configuring Docs](./configuring.md) for info. +All of the users config is specified in `./user-data/conf.yml` - see [Configuring Docs](./configuring.md) for info. It's important to first ensure that there isn't a similar option already available, the new option is definitely necessary, and most importantly that it is fully backwards compatible. Next choose the appropriate section to place it under diff --git a/docs/icons.md b/docs/icons.md index 1a71e1d91a..8f3b0542b3 100644 --- a/docs/icons.md +++ b/docs/icons.md @@ -167,7 +167,7 @@ You can also set an icon by passing in a valid URL pointing to the icons locatio ## Local Icons -You may also want to store your icons locally, bundled within Dashy so that there is no reliance on outside services. This can be done by putting the icons within Dashy's `./public/item-icons/` directory. If you are using Docker, then the easiest option is to map a volume from your host system, for example: `-v /local/image/directory:/app/public/item-icons/`. To reference an icon stored locally, just specify it's name and extension. For example, if my icon was stored in `/app/public/item-icons/maltrail.png`, then I would just set `icon: maltrail.png`. +You may also want to store your icons locally, bundled within Dashy so that there is no reliance on outside services. This can be done by putting the icons within Dashy's `./user-data/item-icons/` directory. If you are using Docker, then the easiest option is to map a volume from your host system, for example: `-v /local/image/directory:/app/user-data/item-icons/`. To reference an icon stored locally, just specify it's name and extension. For example, if my icon was stored in `/app/user-data/item-icons/maltrail.png`, then I would just set `icon: maltrail.png`. You can also use sub-folders within the `item-icons` directory to keep things organized. You would then specify an icon with it's folder name slash image name. For example: `networking/monit.png` @@ -187,7 +187,7 @@ If you don't wish for a given item or section to have an icon, just leave out th ## Icon Collections and Resources -The following websites provide good-quality, free icon sets. To use any of these icons, either copy the link to the raw icon (it should end in `.svg` or `.png`) and paste it as your `icon`, or download and save the icons in `/public/item-icons` / mapped Docker volume. Full credit to the authors, please see the licenses for each service for usage and copyright information. +The following websites provide good-quality, free icon sets. To use any of these icons, either copy the link to the raw icon (it should end in `.svg` or `.png`) and paste it as your `icon`, or download and save the icons in `/user-data/item-icons` / mapped Docker volume. Full credit to the authors, please see the licenses for each service for usage and copyright information. - [Icons for Self-Hosted Apps](https://thehomelab.wiki/books/helpful-tools-resources/page/icons-for-self-hosted-dashboards) - 350+ high-quality icons for commonly self-hosted services - [SVG Box](https://svgbox.net/iconsets/) - Cryptocurrency, social media apps and flag icons diff --git a/docs/management.md b/docs/management.md index 89efd47691..a626ccb085 100644 --- a/docs/management.md +++ b/docs/management.md @@ -30,11 +30,11 @@ _The following article is a primer on managing self-hosted apps. It covers every Although not essential, you will most likely want to provide several assets to your running app. -This is easy to do using [Docker Volumes](https://docs.docker.com/storage/volumes/), which lets you share a file or directory between your host system, and the container. Volumes are specified in the Docker run command, or Docker compose file, using the `--volume` or `-v` flags. The value of which consists of the path to the file / directory on your host system, followed by the destination path within the container. Fields are separated by a colon (`:`), and must be in the correct order. For example: `-v ~/alicia/my-local-conf.yml:/app/public/conf.yml` +This is easy to do using [Docker Volumes](https://docs.docker.com/storage/volumes/), which lets you share a file or directory between your host system, and the container. Volumes are specified in the Docker run command, or Docker compose file, using the `--volume` or `-v` flags. The value of which consists of the path to the file / directory on your host system, followed by the destination path within the container. Fields are separated by a colon (`:`), and must be in the correct order. For example: `-v ~/alicia/my-local-conf.yml:/app/user-data/conf.yml` In Dashy, commonly configured resources include: -- `./public/conf.yml` - Your main application config file +- `./user-data/conf.yml` - Your main application config file - `./public/item-icons` - A directory containing your own icons. This allows for offline access, and better performance than fetching from a CDN - Also within `./public` you'll find standard website assets, including `favicon.ico`, `manifest.json`, `robots.txt`, etc. There's no need to pass these in, but you can do so if you wish - `/src/styles/user-defined-themes.scss` - A stylesheet for applying custom CSS to your app. You can also write your own themes here. @@ -197,7 +197,9 @@ docker run --rm -v some_volume:/volume -v /tmp:/backup alpine sh -c "rm -rf /vol ### Dashy-Specific Backup -Since Dashy is open source, and freely available, providing you're configuration data is passed in as volumes, there shouldn't be any need to backup the main container. Your main config file, and any assets you're using should be kept backed up, preferably in at least two places, and you should ensure that you can easily restore from backup, if needed. +All configuration and dashboard settings are stored in your `user-data/conf.yml` file. If you provide additional assets (like icons, fonts, themes, etc), these will also live in the `user-data` directory. So to backup all Dashy data, this is the only directory you need to backup. + +Since Dashy is open source, there shouldn't be any need to backup the main container. Dashy also has a built-in cloud backup feature, which is free for personal users, and will let you make and restore fully encrypted backups of your config directly through the UI. To learn more, see the [Cloud Backup Docs](/docs/backup-restore.md) @@ -238,7 +240,7 @@ Once you've generated your SSL cert, you'll need to pass it to Dashy. This can b ```bash docker run -d \ - -p 8080:80 \ + -p 8080:8080 \ -v ~/my-private-key.key:/etc/ssl/certs/dashy-priv.key:ro \ -v ~/my-public-key.pem:/etc/ssl/certs/dashy-pub.pem:ro \ lissy93/dashy:latest @@ -276,9 +278,9 @@ services: container_name: Dashy image: lissy93/dashy volumes: - - /root/my-config.yml:/app/public/conf.yml + - /root/my-config.yml:/app/user-data/conf.yml ports: - - 4000:80 + - 4000:8080 environment: - BASE_URL=/my-dashboard restart: unless-stopped @@ -550,7 +552,7 @@ upstream dashy { } server { - listen 80; + listen 8080; server_name dashy.mydomain.com; # Setup SSL @@ -577,7 +579,7 @@ Similarly, a basic `Caddyfile` might look like: ```text dashy.example.com { - reverse_proxy / nginx:80 + reverse_proxy / nginx:8080 } ``` @@ -614,7 +616,7 @@ To prevent known container escape vulnerabilities, which typically end in escala Docker enables you to limit resource consumption (CPU, memory, disk) on a per-container basis. This not only enhances system performance, but also prevents a compromised container from consuming a large amount of resources, in order to disrupt service or perform malicious activities. To learn more, see the [Resource Constraints Docs](https://docs.docker.com/config/containers/resource_constraints/) For example, to run Dashy with max of 1GB ram, and max of 50% of 1 CP core: -`docker run -d -p 8080:80 --cpus=".5" --memory="1024m" lissy93/dashy:latest` +`docker run -d -p 8080:8080 --cpus=".5" --memory="1024m" lissy93/dashy:latest` ### Don't Run as Root @@ -629,7 +631,7 @@ One of the best ways to prevent privilege escalation attacks, is to configure th You can specify a user, using the [`--user` param](https://docs.docker.com/engine/reference/run/#user), and should include the user ID (`UID`), which can be found by running `id -u`, and the and the group ID (`GID`), using `id -g`. With Docker run, you specify it like: -`docker run --user 1000:1000 -p 8080:80 lissy93/dashy` +`docker run --user 1000:1000 -p 8080:8080 lissy93/dashy` Of if you're using Docker-compose, you could use an environmental variable @@ -639,7 +641,7 @@ services: dashy: image: lissy93/dashy user: ${CURRENT_UID} - ports: [ 4000:80 ] + ports: [ 4000:8080 ] ``` And then to set the variable, and start the container, run: `CURRENT_UID=$(id -u):$(id -g) docker-compose up` @@ -659,7 +661,7 @@ version: "3.8" services: dashy: image: lissy93/dashy - ports: [ 4000:80 ] + ports: [ 4000:8080 ] cap_drop: - ALL cap_add: @@ -675,7 +677,7 @@ services: To prevent processes inside the container from getting additional privileges, pass in the `--security-opt=no-new-privileges:true` option to the Docker run command (see [docs](https://docs.docker.com/engine/reference/run/#security-configuration)). Run Command: -`docker run --security-opt=no-new-privileges:true -p 8080:80 lissy93/dashy` +`docker run --security-opt=no-new-privileges:true -p 8080:8080 lissy93/dashy` Docker Compose @@ -701,14 +703,14 @@ You can specify that a specific volume should be read-only by appending `:ro` to ```bash docker run -d \ - -p 8080:80 \ - -v ~/dashy-conf.yml:/app/public/conf.yml \ + -p 8080:8080 \ + -v ~/dashy-conf.yml:/app/user-data/conf.yml \ -v ~/dashy-icons:/app/public/item-icons:ro \ -v ~/dashy-theme.scss:/app/src/styles/user-defined-themes.scss:ro \ lissy93/dashy:latest ``` -You can also prevent a container from writing any changes to volumes on your host's disk, using the `--read-only` flag. Although, for Dashy, you will not be able to write config changes to disk, when edited through the UI with this method. You could make this work, by specifying the config directory as a temp write location, with `--tmpfs /app/public/conf.yml` - but that this will not write the volume back to your host. +You can also prevent a container from writing any changes to volumes on your host's disk, using the `--read-only` flag. Although, for Dashy, you will not be able to write config changes to disk, when edited through the UI with this method. You could make this work, by specifying the config directory as a temp write location, with `--tmpfs /app/user-data/conf.yml` - but that this will not write the volume back to your host. ### Set the Logging Level @@ -778,8 +780,8 @@ Create a new file in `/etc/nginx/sites-enabled/dashy` ```text server { - listen 80; - listen [::]:80; + listen 8080; + listen [::]:8080; root /var/www/dashy/html; index index.html; @@ -898,7 +900,7 @@ Similar to above, you'll first need to fork and clone Dashy to your local system Then, either use Dashy's default [`Dockerfile`](https://github.com/Lissy93/dashy/blob/master/Dockerfile) as is, or modify it according to your needs. -To build and deploy locally, first build the app with: `docker build -t dashy .`, and then start the app with `docker run -p 8080:80 --name my-dashboard dashy`. Or modify the `docker-compose.yml` file, replacing `image: lissy93/dashy` with `build: .` and run `docker compose up`. +To build and deploy locally, first build the app with: `docker build -t dashy .`, and then start the app with `docker run -p 8080:8080 --name my-dashboard dashy`. Or modify the `docker-compose.yml` file, replacing `image: lissy93/dashy` with `build: .` and run `docker compose up`. Your container should now be running, and will appear in the list when you run `docker container ls –a`. If you'd like to enter the container, run `docker exec -it [container-id] /bin/ash`. diff --git a/docs/privacy.md b/docs/privacy.md index 917fb8c559..05c44c97d7 100644 --- a/docs/privacy.md +++ b/docs/privacy.md @@ -192,7 +192,7 @@ The following section outlines all data that is stored in the browsers, as cooki > [Local storage](https://developer.mozilla.org/en-US/docs/Web/API/Window/localStorage) is persisted between sessions, and only deleted when manually removed - `LANGUAGE` - The locale to show app text in -- `HIDE_WELCOME_BANNER` - Set to true once user dismissed welcome message, so that it's not shown again +- `HIDE_INFO_NOTIFICATION` - Set to true once user dismissed welcome message, so that it's not shown again - `LAYOUT_ORIENTATION` - Preferred section layout, either horizontal, vertical or auto - `COLLAPSE_STATE` - Remembers which sections are collapsed - `ICON_SIZE` - Size of items, either small, medium or large diff --git a/docs/quick-start.md b/docs/quick-start.md index 3bfa083ffa..1fa0060e3c 100644 --- a/docs/quick-start.md +++ b/docs/quick-start.md @@ -2,7 +2,7 @@ Welcome to Dashy! So glad you're here 😊 In a couple of minutes, you'll have your new dashboard up and running 🚀 -**TLDR;** Run `docker run -p 8080:80 lissy93/dashy`, then open `http://localhost:8080` +**TLDR;** Run `docker run -p 8080:8080 lissy93/dashy`, then open `http://localhost:8080` --- @@ -19,8 +19,8 @@ To pull the latest image, and build and start the app run: ```bash docker run -d \ - -p 8080:80 \ - -v ~/my-conf.yml:/app/public/conf.yml \ + -p 8080:8080 \ + -v ~/my-conf.yml:/app/user-data/conf.yml \ --name my-dashboard \ --restart=always \ lissy93/dashy:latest @@ -35,7 +35,7 @@ Your dashboard should now be up and running at `http://localhost:8080` (or your ## 3. Configure Now that you've got Dashy running, you are going to want to set it up with your own content. -Config is written in [YAML Format](https://yaml.org/), and saved in [`/public/conf.yml`](https://github.com/Lissy93/dashy/blob/master/public/conf.yml). +Config is written in [YAML Format](https://yaml.org/), and saved in [`/user-data/conf.yml`](https://github.com/Lissy93/dashy/blob/master/user-data/conf.yml). The format on the config file is pretty straight forward. There are three root attributes: - [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo) - Dashboard meta data, like title, description, nav bar links and footer text @@ -72,7 +72,7 @@ sections: # An array of sections Notes: - You can use a Docker volume to pass a config file from your host system to the container - - E.g. `-v ./host-system/my-local-conf.yml:/app/public/conf.yml` + - E.g. `-v ./host-system/my-local-conf.yml:/app/user-data/conf.yml` - It's also possible to edit your config directly through the UI, and changes will be saved in this file - Check your config against Dashy's schema, with `docker exec -it [container-id] yarn validate-config` - You might find it helpful to look at some examples, a collection of which can be [found here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10) @@ -118,7 +118,7 @@ yarn build # Build the app yarn start # Start the app ``` -Then edit `./public/conf.yml` and rebuild the app with `yarn build` +Then edit `./user-data/conf.yml` and rebuild the app with `yarn build` --- @@ -129,7 +129,7 @@ Don't have a server? No problem! You can run Dashy for free on Netlify (as well 1. Fork Dashy's repository on GitHub 2. [Log in](app.netlify.com/login/) to Netlify with GitHub 3. Click "New site from Git" and select your forked repo, then click **Deploy**! -4. You can then edit the config in `./public/conf.yml` in your repo, and Netlify will rebuild the app +4. You can then edit the config in `./user-data/conf.yml` in your repo, and Netlify will rebuild the app --- diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 44fcfcbcbb..a0f7f0e770 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -156,7 +156,7 @@ If you're getting an error about scenarios, then you've likely installed the wro Alternatively, as a workaround, you have several options: - Try using [NPM](https://www.npmjs.com/get-npm) instead: So clone, cd, then run `npm install`, `npm run build` and `npm start` -- Try using [Docker](https://www.docker.com/get-started) instead, and all of the system setup and dependencies will already be taken care of. So from within the directory, just run `docker build -t lissy93/dashy .` to build, and then use docker start to run the project, e.g: `docker run -it -p 8080:80 lissy93/dashy` (see the [deploying docs](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md#deploy-with-docker) for more info) +- Try using [Docker](https://www.docker.com/get-started) instead, and all of the system setup and dependencies will already be taken care of. So from within the directory, just run `docker build -t lissy93/dashy .` to build, and then use docker start to run the project, e.g: `docker run -it -p 8080:8080 lissy93/dashy` (see the [deploying docs](https://github.com/Lissy93/dashy/blob/master/docs/deployment.md#deploy-with-docker) for more info) --- @@ -234,7 +234,7 @@ Version 2.0.4 introduced changes to how the config is read, and the app is build ```yaml volumes: -- /srv/dashy/conf.yml:/app/public/conf.yml +- /srv/dashy/conf.yml:/app/user-data/conf.yml - /srv/dashy/item-icons:/app/public/item-icons ``` @@ -273,12 +273,12 @@ See also: #479, #409, #507, #491, #341, #520 Error response from daemon: OCI runtime create failed: container_linux.go:380: starting container process caused: process_linux.go:545: container init caused: rootfs_linux.go:76: mounting "/home/ubuntu/my-conf.yml" to rootfs at -"/app/public/conf.yml" caused: mount through procfd: not a directory: +"/app/user-data/conf.yml" caused: mount through procfd: not a directory: unknown: Are you trying to mount a directory onto a file (or vice-versa)? Check if the specified host path exists and is the expected type. ``` -If you get an error similar to the one above, you are mounting a directory to the config file's location, when a plain file is expected. Create a YAML file, (`touch my-conf.yml`), populate it with a sample config, then pass it as a volume: `-v ./my-local-conf.yml:/app/public/conf.yml` +If you get an error similar to the one above, you are mounting a directory to the config file's location, when a plain file is expected. Create a YAML file, (`touch my-conf.yml`), populate it with a sample config, then pass it as a volume: `-v ./my-local-conf.yml:/app/user-data/conf.yml` --- diff --git a/docs/widgets.md b/docs/widgets.md index 516adf6238..a657693a6c 100644 --- a/docs/widgets.md +++ b/docs/widgets.md @@ -92,6 +92,7 @@ Dashy has support for displaying dynamic content in the form of widgets. There a - [Widget Usage Guide](#widget-usage-guide) - [Continuous Updates](#continuous-updates) - [Proxying Requests](#proxying-requests) + - [Handling Secrets](#handling-secrets) - [Setting Timeout](#setting-timeout) - [Adding Labels](#adding-labels) - [Ignoring Errors](#ignoring-errors) @@ -1554,6 +1555,19 @@ Displays the number of queries blocked by [Pi-Hole](https://pi-hole.net/). apiKey: xxxxxxxxxxxxxxxxxxxxxxx ``` +> [!TIP] +> In order to avoid leaking secret data, both `hostname` and `apiKey` can leverage environment variables. Simply pass the name of the variable, which MUST start with `VUE_APP_`. + +```yaml +- type: pi-hole-stats + options: + hostname: VUE_APP_pihole_ip + apiKey: VUE_APP_pihole_key +``` + +> [!IMPORTANT] +> You will need to restart the server (or the docker image) if adding/editing an env var for this to be refreshed. + #### Info - **CORS**: đŸŸĸ Enabled @@ -2843,6 +2857,32 @@ Vary: Origin --- +### Handling Secrets + +Some widgets require you to pass potentially sensetive info such as API keys. The `conf.yml` is not ideal for this, as it's stored in plaintext. +Instead, for secrets you should use environmental vairables. + +You can do this, by setting the environmental variable name as the value, instead of the actual key, and then setting that env var in your container or local environment. + +The key can be named whatever you like, but it must start with `VUE_APP_` (to be picked up by Vue). If you need to update any of these values, a rebuild is required (this can be done under the Config menu in the UI, or by running `yarn build` then restarting the container). + +For more infomation about setting and managing your environmental variables, see [Management Docs --> Environmental Variables](/docs/management.md#passing-in-environmental-variables). + +For example: + +```yaml +- type: weather + options: + apiKey: VUE_APP_WEATHER_TOKEN + city: London + units: metric + hideDetails: true +``` + +Then, set `VUE_APP_WEATHER_TOKEN='xxx'` + +--- + ### Setting Timeout If the endpoint you are requesting data from is slow to respond, you may see a timeout error in the console. This can easily be fixed by specifying the `timeout` property on the offending widget. This should be an integer value, in milliseconds. By default timeout is `2500` ms (2ÂŊ seconds). diff --git a/package.json b/package.json index 9227808807..27573a1909 100644 --- a/package.json +++ b/package.json @@ -1,18 +1,17 @@ { "name": "dashy", - "version": "2.1.2", + "version": "3.0.0", "license": "MIT", "main": "server", "author": "Alicia Sykes (https://aliciasykes.com)", "scripts": { "start": "node server", - "dev": "vue-cli-service serve", + "dev": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service serve", "build": "NODE_OPTIONS=--openssl-legacy-provider vue-cli-service build", "lint": "vue-cli-service lint", "pm2-start": "npx pm2 start server.js", "build-watch": "vue-cli-service build --watch --mode production", - "watch-config": "node services/watch-for-changes", - "build-and-start": "NODE_OPTIONS=--openssl-legacy-provider npm-run-all --parallel build-watch start", + "build-and-start": "NODE_OPTIONS=--openssl-legacy-provider npm-run-all --parallel build start", "validate-config": "node services/config-validator", "health-check": "node services/healthcheck", "dependency-audit": "npx improved-yarn-audit --ignore-dev-deps" @@ -53,15 +52,18 @@ "@vue/cli-plugin-babel": "^4.5.15", "@vue/cli-plugin-eslint": "^4.5.15", "@vue/cli-plugin-pwa": "^4.5.15", - "@vue/cli-service": "^4.5.15", + "@vue/cli-plugin-typescript": "^5.0.8", + "@vue/cli-service": "^4.5.19", "@vue/eslint-config-standard": "^4.0.0", "babel-eslint": "^10.0.1", + "copy-webpack-plugin": "6.4.0", "eslint": "^6.8.0", "eslint-config-airbnb": "^18.0.1", "eslint-plugin-vue": "^7.9.0", "npm-run-all": "^4.1.5", "sass": "^1.38.0", "sass-loader": "^7.1.0", + "typescript": "^5.4.4", "vue-cli-plugin-yaml": "^1.0.2", "vue-svg-loader": "^0.16.0", "vue-template-compiler": "^2.7.0" diff --git a/server.js b/server.js index 1a7b2c9ea9..b0a8335f4a 100644 --- a/server.js +++ b/server.js @@ -18,7 +18,9 @@ const history = require('connect-history-api-fallback'); /* Kick of some basic checks */ require('./services/update-checker'); // Checks if there are any updates available, prints message -require('./services/config-validator'); // Include and kicks off the config file validation script + +let config = {}; // setup the config +config = require('./services/config-validator'); // Include and kicks off the config file validation script /* Include route handlers for API endpoints */ const statusCheck = require('./services/status-check'); // Used by the status check feature, uses GET @@ -27,6 +29,7 @@ const rebuild = require('./services/rebuild-app'); // A script to programmatical const systemInfo = require('./services/system-info'); // Basic system info, for resource widget const sslServer = require('./services/ssl-server'); // TLS-enabled web server const corsProxy = require('./services/cors-proxy'); // Enables API requests to CORS-blocked services +const getUser = require('./services/get-user'); // Enables server side user lookup /* Helper functions, and default config */ const printMessage = require('./services/print-message'); // Function to print welcome msg on start @@ -35,12 +38,15 @@ const ENDPOINTS = require('./src/utils/defaults').serviceEndpoints; // API endpo /* 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); +/* Checks env var for port. If undefined, will use Port 8080 for Docker, or 4000 for metal */ +const port = process.env.PORT || (isDocker ? 8080 : 4000); /* Checks env var for host. If undefined, will use 0.0.0.0 */ const host = process.env.HOST || '0.0.0.0'; +/* Indicates for the webpack config, that running as a server */ +process.env.IS_SERVER = 'True'; + /* Attempts to get the users local IP, used as part of welcome message */ const getLocalIp = () => { const dnsLookup = util.promisify(dns.lookup); @@ -71,12 +77,8 @@ const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, n const app = express() // Load SSL redirection middleware .use(sslServer.middleware) - // Serves up static files - .use(express.static(path.join(__dirname, 'dist'))) - .use(express.static(path.join(__dirname, 'public'), { index: 'initialization.html' })) // Load middlewares for parsing JSON, and supporting HTML5 history routing .use(express.json({ limit: '1mb' })) - .use(history()) // GET endpoint to run status of a given URL with GET request .use(ENDPOINTS.statusCheck, (req, res) => { try { @@ -87,10 +89,11 @@ const app = express() printWarning(`Error running status check for ${req.url}\n`, e); } }) - // POST Endpoint used to save config, by writing conf.yml to disk + // POST Endpoint used to save config, by writing config file to disk .use(ENDPOINTS.save, method('POST', (req, res) => { try { saveConfig(req.body, (results) => { res.end(results); }); + config = req.body.config; // update the config } catch (e) { printWarning('Error writing config file to disk', e); res.end(JSON.stringify({ success: false, message: e })); @@ -122,6 +125,20 @@ const app = express() res.end(JSON.stringify({ success: false, message: e })); } }) + // GET endpoint to return user info + .use(ENDPOINTS.getUser, (req, res) => { + try { + const user = getUser(config, req); + res.end(JSON.stringify(user)); + } catch (e) { + res.end(JSON.stringify({ success: false, message: e })); + } + }) + // Serves up static files + .use(express.static(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data'))) + .use(express.static(path.join(__dirname, 'dist'))) + .use(express.static(path.join(__dirname, 'public'), { index: 'initialization.html' })) + .use(history()) // If no other route is matched, serve up the index.html with a 404 status .use((req, res) => { res.status(404).sendFile(path.join(__dirname, 'dist', 'index.html')); diff --git a/services/config-validator.js b/services/config-validator.js index 48c1e82772..f7786e0ec2 100644 --- a/services/config-validator.js +++ b/services/config-validator.js @@ -98,11 +98,14 @@ const printFileReadError = (e) => { } }; +let config = {}; + try { // Try to open and parse the YAML file - const config = yaml.load(fs.readFileSync('./public/conf.yml', 'utf8')); + config = yaml.load(fs.readFileSync(`./${process.env.USER_DATA_DIR || 'user-data'}/conf.yml`, 'utf8')); validate(config); } catch (e) { // Something went very wrong... setIsValidVariable(false); logToConsole(bigError()); printFileReadError(e); } +module.exports = config; diff --git a/services/get-user.js b/services/get-user.js new file mode 100644 index 0000000000..b9c9e45795 --- /dev/null +++ b/services/get-user.js @@ -0,0 +1,15 @@ +module.exports = (config, req) => { + try { + if ( config.appConfig.auth.enableHeaderAuth ) { + const userHeader = config.appConfig.auth.headerAuth.userHeader; + const proxyWhitelist = config.appConfig.auth.headerAuth.proxyWhitelist; + if ( proxyWhitelist.includes(req.socket.remoteAddress) ) { + return { "success": true, "user": req.headers[userHeader.toLowerCase()] }; + } + } + return {}; + } catch (e) { + console.warn("Error get-user: ", e); + return { 'success': false }; + } +}; \ No newline at end of file diff --git a/services/healthcheck.js b/services/healthcheck.js index d0ba24211f..a869b64f7d 100644 --- a/services/healthcheck.js +++ b/services/healthcheck.js @@ -6,11 +6,17 @@ const isSsl = !!process.env.SSL_PRIV_KEY_PATH && !!process.env.SSL_PUB_KEY_PATH; +// eslint-disable-next-line import/no-dynamic-require const http = require(isSsl ? 'https' : 'http'); /* Location of the server to test */ const isDocker = !!process.env.IS_DOCKER; -const port = isSsl ? (process.env.SSL_PORT || (isDocker ? 443 : 4001)) : (process.env.PORT || (isDocker ? 80 : 4000)); + +/* Get the port to use (depending on, if docker, if SSL) */ +const sslPort = process.env.SSL_PORT || (isDocker ? 443 : 4001); +const normalPort = process.env.PORT || (isDocker ? 8080 : 4000); +const port = isSsl ? sslPort : normalPort; + const host = process.env.HOST || '0.0.0.0'; const timeout = 2000; @@ -18,7 +24,9 @@ const agent = new http.Agent({ rejectUnauthorized: false, // Allow self-signed certificates }); -const requestOptions = { host, port, timeout, agent }; +const requestOptions = { + host, port, timeout, agent, +}; const startTime = new Date(); // Initialize timestamp to calculate time taken diff --git a/services/print-message.js b/services/print-message.js index 69feb4de76..0013b2ab78 100644 --- a/services/print-message.js +++ b/services/print-message.js @@ -32,7 +32,7 @@ module.exports = (ip, port, isDocker) => { } else { // Prepare message for users running app on bare metal msg = `${chars.GREEN}┏${line(75)}┓${chars.BR}` - + `┃ ${chars.CYAN}Welcome to Dashy! 🚀${blanks(55)}${chars.GREEN}┃${chars.BR}` + + `┃ ${chars.CYAN}Welcome to Dashy! 🚀${blanks(54)}${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}` + `┗${line(75)}┛${chars.BR}${chars.BR}${chars.RESET}`; diff --git a/services/save-config.js b/services/save-config.js index 1af0d2993a..f946be54ae 100644 --- a/services/save-config.js +++ b/services/save-config.js @@ -18,15 +18,15 @@ module.exports = async (newConfig, render) => { // Define constants for the config file const settings = { - defaultLocation: './public/', + defaultLocation: process.env.USER_DATA_DIR || './user-data/', defaultFile: 'conf.yml', filename: 'conf', backupDenominator: '.backup.yml', }; // Make the full file name and path to save the backup config file - const backupFilePath = path.normalize(process.env.BACKUP_DIR || settings.defaultLocation) - + `/${usersFileName || settings.filename}-` + const backupFilePath = `${path.normalize(process.env.BACKUP_DIR || settings.defaultLocation) + }/${usersFileName || settings.filename}-` + `${Math.round(new Date() / 1000)}${settings.backupDenominator}`; // The path where the main conf.yml should be read and saved to @@ -48,12 +48,12 @@ module.exports = async (newConfig, render) => { // Makes a backup of the existing config file await fsPromises .copyFile(defaultFilePath, backupFilePath) - .catch((error) => render(getRenderMessage(false, `Unable to backup conf.yml: ${error}`))); + .catch((error) => render(getRenderMessage(false, `Unable to backup ${settings.defaultFile}: ${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 to conf.yml: ${error}`))); + .catch((error) => render(getRenderMessage(false, `Unable to write to ${settings.defaultFile}: ${error}`))); // If successful, then render hasn't yet been called- call it await render(getRenderMessage(true)); diff --git a/services/watch-for-changes.js b/services/watch-for-changes.js deleted file mode 100644 index a66000b24f..0000000000 --- a/services/watch-for-changes.js +++ /dev/null @@ -1,78 +0,0 @@ -const fs = require('fs'); -const { exec } = require('child_process'); -const path = require('path'); -const crypto = require('crypto'); - -// Default location of config file in container -const configFileName = '../public/conf.yml'; -// Real path of config file in container -const configFilePath = path.resolve(__dirname, configFileName); -// Amount of time to ignore file after change detected -const debounceTimeMs = 2000; - -// Store current timeout -let timeout = null; -// Store last hash of file -let lastHash = null; - -/** - * Calculate hash of file, used for de-bounce mechanism to - * prevent successive updates if file content not changed - */ -const hashFileContent = (filePath) => { - const content = fs.readFileSync(filePath, 'utf8'); - return crypto.createHash('sha256').update(content).digest('hex'); -}; - -/** - * Just logs a given message to terminal so user knows what's happening - */ -const logInfo = (message, msgLevel = 'OUTPUT') => { - const RESET = '\x1b[0m'; - let logLevels = {}; - switch (msgLevel) { - case 'ERROR': logLevels = { col: '\x1b[31m', func: console.error }; break; - case 'WARNING': logLevels = { col: '\x1b[33m', func: console.warn }; break; - case 'INFO': logLevels = { col: '\x1b[36m', func: console.info }; break; - case 'SUCCESS': logLevels = { col: '\x1b[32m', func: console.log }; break; - default: logLevels = { col: RESET, func: console.log }; - } - logLevels.func(`${logLevels.col}\x1b[1m[${msgLevel}]${RESET} ${logLevels.col}${message}${RESET}\n`); -}; - -// Log initial message to user -logInfo(`When '${configFileName}' is updated, a rebuild will be triggered.\n`); - -/** - * Code to be executed when a watch event is triggered - * Will check correctly expected file and time frame, - * then ensure the hash is different from last hash, - * and then trigger -rebuild of frontend with yarn build - * outputting the stdrout and stderr to user's terminal - */ -const watchAction = (eventType, filename) => { - if (filename && eventType === 'change') { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => { - const currentHash = hashFileContent(configFilePath); - if (currentHash !== lastHash) { - lastHash = currentHash; - logInfo(`${filename} file Changed, running build...`); - exec('yarn build', (error, stdout, stderr) => { - if (error) { - logInfo(error, 'ERROR'); - return; - } - logInfo(stdout); - logInfo(stderr, 'WARNING'); - logInfo('Build completed successfully.\n', 'SUCCESS'); - }); - } else { - logInfo(`${filename} file Detected change, but content is the same. Skipping....`, 'WARNING'); - } - }, debounceTimeMs); - } -}; - -// Watch given config path, with the watch action function -fs.watch(configFilePath, watchAction); diff --git a/src/App.vue b/src/App.vue index 56e49cb940..4cfd5d5db2 100644 --- a/src/App.vue +++ b/src/App.vue @@ -64,7 +64,7 @@ export default { return this.$store.getters.pageInfo; }, sections() { - return this.$store.getters.pageInfo; + return this.$store.getters.sections; }, visibleComponents() { return this.$store.getters.visibleComponents; diff --git a/src/assets/locales/en.json b/src/assets/locales/en.json index 6c3e348161..d6b119f912 100644 --- a/src/assets/locales/en.json +++ b/src/assets/locales/en.json @@ -171,9 +171,9 @@ "status-fail-msg": "Task Failed", "success-msg-disk": "Config file written to disk successfully", "success-msg-local": "Local changes saved successfully", - "success-note-l1": "The app should rebuild automatically.", - "success-note-l2": "This may take up to a minute.", - "success-note-l3": "You will need to refresh the page for changes to take effect.", + "success-note-l1": "You will need to refresh the page for changes to take effect.", + "success-note-l2": "", + "success-note-l3": "", "error-msg-save-mode": "Please select a Save Mode: Local or File", "error-msg-cannot-save": "An error occurred saving config", "error-msg-bad-json": "Error in JSON, possibly malformed", @@ -182,9 +182,9 @@ }, "app-rebuild": { "title": "Rebuild Application", - "rebuild-note-l1": "A rebuild is required for changes written to the conf.yml file to take effect.", - "rebuild-note-l2": "This should happen automatically, but if it hasn't, you can manually trigger it here.", - "rebuild-note-l3": "This is not required for modifications stored locally.", + "rebuild-note-l1": "A rebuild is no longer required for changes to take effect.", + "rebuild-note-l2": "Some changes (entry-point, and auth settings) are read at build-time. So to apply these, you should trigger a rebuild here.", + "rebuild-note-l3": "Note that this is only available on Node and Docker installations, not via statically deployed instances.", "rebuild-button": "Start Build", "rebuilding-status-1": "Building...", "rebuilding-status-2": "This may take a few minutes", diff --git a/src/components/Configuration/CloudBackupRestore.vue b/src/components/Configuration/CloudBackupRestore.vue index f8d75a6527..9126dc48a9 100644 --- a/src/components/Configuration/CloudBackupRestore.vue +++ b/src/components/Configuration/CloudBackupRestore.vue @@ -155,12 +155,23 @@ export default { }, /* When restored data is revieved, then save to local storage, and apply it in state */ applyRestoredData(config, backupId) { - // Store restored data in local storage - localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections)); - localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig)); - localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo)); - if (config.appConfig.theme) { - localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme); + const isSubPage = !!this.$store.state.currentConfigInfo.confId; + if (isSubPage) { // Apply to sub-page only + const subConfigId = this.$store.state.currentConfigInfo.confId; + const sectionStorageKey = `${localStorageKeys.CONF_SECTIONS}-${subConfigId}`; + const pageInfoStorageKey = `${localStorageKeys.PAGE_INFO}-${subConfigId}`; + const themeStoreKey = `${localStorageKeys.THEME}-${subConfigId}`; + localStorage.setItem(sectionStorageKey, JSON.stringify(config.sections)); + localStorage.setItem(pageInfoStorageKey, JSON.stringify(config.pageInfo)); + localStorage.setItem(themeStoreKey, config.appConfig.theme); + } else { // Apply to main config + localStorage.setItem(localStorageKeys.CONF_SECTIONS, JSON.stringify(config.sections)); + localStorage.setItem(localStorageKeys.APP_CONFIG, JSON.stringify(config.appConfig)); + localStorage.setItem(localStorageKeys.PAGE_INFO, JSON.stringify(config.pageInfo)); + localStorage.setItem(localStorageKeys.CONF_PAGES, JSON.stringify(config.pages || [])); + if (config.appConfig.theme) { + localStorage.setItem(localStorageKeys.THEME, config.appConfig.theme); + } } // Save hashed token in local storage this.setBackupIdLocally(backupId, this.restorePassword); diff --git a/src/components/Configuration/ConfigContainer.vue b/src/components/Configuration/ConfigContainer.vue index 9b08f0289f..67e70c2320 100644 --- a/src/components/Configuration/ConfigContainer.vue +++ b/src/components/Configuration/ConfigContainer.vue @@ -47,16 +47,17 @@

{{ getLanguage() }}

-

- Using Config From
- {{ $store.state.currentConfigInfo.confPath }} + +

+ Using config from + {{ configPath }}

{{ $t('config.disabled-note') }}

-
+
{{ $t('config.backup-note') }}
@@ -116,6 +117,11 @@ export default { enableConfig() { return this.$store.getters.permissions.allowViewConfig; }, + configPath() { + return this.$store.state.currentConfigInfo?.confPath + || process.env.VUE_APP_CONFIG_PATH + || '/conf.yml'; + }, }, components: { Button, @@ -248,8 +254,12 @@ a.hyperlink-wrapper { p.app-version, p.language, p.config-location { margin: 0.5rem auto; font-size: 1rem; - color: var(--transparent-white-50); + color: var(--config-settings-color); cursor: default; + opacity: var(--dimming-factor); + a { + color: var(--config-settings-color); + } } div.code-container { diff --git a/src/components/Configuration/JsonEditor.vue b/src/components/Configuration/JsonEditor.vue index 65c21556f7..0267d2a2aa 100644 --- a/src/components/Configuration/JsonEditor.vue +++ b/src/components/Configuration/JsonEditor.vue @@ -143,7 +143,11 @@ export default { this.$modal.hide(modalNames.CONF_EDITOR); }, writeToDisk() { - this.writeConfigToDisk(this.config); + const newData = this.jsonData; + this.writeConfigToDisk(newData); + // this.$store.commit(StoreKeys.SET_APP_CONFIG, newData.appConfig); + this.$store.commit(StoreKeys.SET_PAGE_INFO, newData.pageInfo); + this.$store.commit(StoreKeys.SET_SECTIONS, newData.sections); }, saveLocally() { const msg = this.$t('interactive-editor.menu.save-locally-warning'); diff --git a/src/components/InteractiveEditor/ExportConfigMenu.vue b/src/components/InteractiveEditor/ExportConfigMenu.vue index 6d316e5d46..239703cb92 100644 --- a/src/components/InteractiveEditor/ExportConfigMenu.vue +++ b/src/components/InteractiveEditor/ExportConfigMenu.vue @@ -22,6 +22,14 @@ + +
+

Config Location

+

+ The base config file you are currently using is + {{ configPath }} +

+

{{ $t('interactive-editor.export.view-title') }}

@@ -61,6 +69,11 @@ export default { allowViewConfig() { return this.$store.getters.permissions.allowViewConfig; }, + configPath() { + return this.$store.state.currentConfigInfo?.confPath + || process.env.VUE_APP_CONFIG_PATH + || '/conf.yml'; + }, }, methods: { convertJsonToYaml() { @@ -121,6 +134,13 @@ export default { border-bottom: 1px dashed var(--interactive-editor-color); button { margin: 0 1rem; } } + .config-path-info { + p, a { + color: var(--interactive-editor-color); + font-size: 1.2rem; + } + border-bottom: 1px dashed var(--interactive-editor-color); + } .config-tree-view { padding: 0.5rem; font-family: var(--font-monospace); diff --git a/src/components/PageStrcture/Footer.vue b/src/components/PageStrcture/Footer.vue index ab2ea2ea26..d213f1bba4 100644 --- a/src/components/PageStrcture/Footer.vue +++ b/src/components/PageStrcture/Footer.vue @@ -1,16 +1,13 @@ @@ -23,13 +20,20 @@ export default { name: 'Footer', props: { text: String, - authorName: { type: String, default: 'Alicia Sykes' }, - authorUrl: { type: String, default: 'https://aliciasykes.com' }, - license: { type: String, default: 'MIT' }, - licenseUrl: { type: String, default: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17' }, - date: { type: String, default: `${new Date().getFullYear()}` }, - showCopyright: { type: Boolean, default: true }, - repoUrl: { type: String, default: 'https://github.com/lissy93/dashy' }, + }, + data() { + return { + defaultInfo: { + authorName: 'Alicia Sykes', + authorUrl: 'https://as93.net', + license: 'MIT', + licenseUrl: 'https://gist.github.com/Lissy93/143d2ee01ccc5c052a17', + date: `${new Date().getFullYear()}`, + repoUrl: 'https://github.com/lissy93/dashy', + repoName: 'Lissy93/Dashy', + projectUrl: 'https://dashy.to', + }, + }; }, computed: { visible() { @@ -56,7 +60,7 @@ footer { display: none; } span.path-to-config { - float: right; + float: left; font-size: 0.75rem; margin: 0.1rem 0.5rem 0 0; opacity: var(--dimming-factor); diff --git a/src/components/PageStrcture/PageTitle.vue b/src/components/PageStrcture/PageTitle.vue index 26c7cceffa..4c8f3d6664 100644 --- a/src/components/PageStrcture/PageTitle.vue +++ b/src/components/PageStrcture/PageTitle.vue @@ -66,7 +66,7 @@ export default { span.subtitle { color: var(--heading-text-color); font-style: italic; - text-shadow: 1px 1px 2px #130f23; + text-shadow: 1px 1px 2px #130f2347; opacity: var(--dimming-factor); } img.site-logo { diff --git a/src/components/Settings/KeyboardShortcutInfo.vue b/src/components/Settings/LocalConfigWarning.vue similarity index 55% rename from src/components/Settings/KeyboardShortcutInfo.vue rename to src/components/Settings/LocalConfigWarning.vue index 6659612df5..af38d1509b 100644 --- a/src/components/Settings/KeyboardShortcutInfo.vue +++ b/src/components/Settings/LocalConfigWarning.vue @@ -1,36 +1,70 @@ diff --git a/src/components/Widgets/AdGuardDnsInfo.vue b/src/components/Widgets/AdGuardDnsInfo.vue index a1b1fcb87b..beb187259d 100644 --- a/src/components/Widgets/AdGuardDnsInfo.vue +++ b/src/components/Widgets/AdGuardDnsInfo.vue @@ -26,7 +26,7 @@ export default { /* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */ hostname() { if (!this.options.hostname) this.error('You must specify the path to your AdGuard server'); - return this.options.hostname; + return this.parseAsEnvVar(this.options.hostname); }, showFullInfo() { return this.options.showFullInfo; @@ -39,7 +39,9 @@ export default { }, authHeaders() { if (this.options.username && this.options.password) { - const encoded = window.btoa(`${this.options.username}:${this.options.password}`); + const password = this.parseAsEnvVar(this.options.password); + const username = this.parseAsEnvVar(this.options.username); + const encoded = window.btoa(`${username}:${password}`); return { Authorization: `Basic ${encoded}` }; } return {}; diff --git a/src/components/Widgets/AdGuardFilterStatus.vue b/src/components/Widgets/AdGuardFilterStatus.vue index bf1899fec7..5997086f1e 100644 --- a/src/components/Widgets/AdGuardFilterStatus.vue +++ b/src/components/Widgets/AdGuardFilterStatus.vue @@ -38,7 +38,7 @@ export default { /* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */ hostname() { if (!this.options.hostname) this.error('You must specify the path to your AdGuard server'); - return this.options.hostname; + return this.parseAsEnvVar(this.options.hostname); }, showOnOffStatusOnly() { return this.options.showOnOffStatusOnly; @@ -48,7 +48,9 @@ export default { }, authHeaders() { if (this.options.username && this.options.password) { - const encoded = window.btoa(`${this.options.username}:${this.options.password}`); + const username = this.parseAsEnvVar(this.options.username); + const password = this.parseAsEnvVar(this.options.password); + const encoded = window.btoa(`${username}:${password}`); return { Authorization: `Basic ${encoded}` }; } return {}; diff --git a/src/components/Widgets/AdGuardStats.vue b/src/components/Widgets/AdGuardStats.vue index f1123546ca..2c649d305a 100644 --- a/src/components/Widgets/AdGuardStats.vue +++ b/src/components/Widgets/AdGuardStats.vue @@ -20,14 +20,16 @@ export default { /* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */ hostname() { if (!this.options.hostname) this.error('You must specify the path to your AdGuard server'); - return this.options.hostname; + return this.parseAsEnvVar(this.options.hostname); }, endpoint() { return `${this.hostname}/control/stats`; }, authHeaders() { if (this.options.username && this.options.password) { - const encoded = window.btoa(`${this.options.username}:${this.options.password}`); + const username = this.parseAsEnvVar(this.options.username); + const password = this.parseAsEnvVar(this.options.password); + const encoded = window.btoa(`${username}:${password}`); return { Authorization: `Basic ${encoded}` }; } return {}; diff --git a/src/components/Widgets/AdGuardTopDomains.vue b/src/components/Widgets/AdGuardTopDomains.vue index d19ff7d7f7..6975945761 100644 --- a/src/components/Widgets/AdGuardTopDomains.vue +++ b/src/components/Widgets/AdGuardTopDomains.vue @@ -36,11 +36,13 @@ export default { /* URL/ IP or hostname to the AdGuardHome instance, without trailing slash */ hostname() { if (!this.options.hostname) this.error('You must specify the path to your AdGuard server'); - return this.options.hostname; + return this.parseAsEnvVar(this.options.hostname); }, authHeaders() { if (this.options.username && this.options.password) { - const encoded = window.btoa(`${this.options.username}:${this.options.password}`); + const username = this.parseAsEnvVar(this.options.username); + const password = this.parseAsEnvVar(this.options.password); + const encoded = window.btoa(`${username}:${password}`); return { Authorization: `Basic ${encoded}` }; } return {}; diff --git a/src/components/Widgets/AnonAddy.vue b/src/components/Widgets/AnonAddy.vue index 0e466c05ed..eb5d4d6ce0 100644 --- a/src/components/Widgets/AnonAddy.vue +++ b/src/components/Widgets/AnonAddy.vue @@ -113,7 +113,7 @@ export default { }, computed: { hostname() { - return this.options.hostname || widgetApiEndpoints.anonAddy; + return this.parseAsEnvVar(this.options.hostname) || widgetApiEndpoints.anonAddy; }, apiVersion() { return this.options.apiVersion || 'v1'; @@ -132,7 +132,7 @@ export default { }, apiKey() { if (!this.options.apiKey) this.error('An apiKey is required'); - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, hideMeta() { return this.options.hideMeta; diff --git a/src/components/Widgets/BlacklistCheck.vue b/src/components/Widgets/BlacklistCheck.vue index ad5975b9d0..7fee0094ec 100644 --- a/src/components/Widgets/BlacklistCheck.vue +++ b/src/components/Widgets/BlacklistCheck.vue @@ -35,7 +35,7 @@ export default { }, apiKey() { if (!this.options.apiKey) this.error('Missing API Key'); - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, endpoint() { return `${widgetApiEndpoints.blacklistCheck}/${this.ipAddress}`; diff --git a/src/components/Widgets/CodeStats.vue b/src/components/Widgets/CodeStats.vue index 61c3ae699b..29be3b1942 100644 --- a/src/components/Widgets/CodeStats.vue +++ b/src/components/Widgets/CodeStats.vue @@ -38,12 +38,12 @@ export default { /* The username to fetch data from - REQUIRED */ username() { if (!this.options.username) this.error('You must specify a username'); - return this.options.username; + return this.parseAsEnvVar(this.options.username); }, /* Optionally override hostname, if using a self-hosted instance */ hostname() { if (this.options.hostname) return this.options.hostname; - return widgetApiEndpoints.codeStats; + return this.parseAsEnvVar(widgetApiEndpoints.codeStats); }, hideMeta() { return this.options.hideMeta || false; diff --git a/src/components/Widgets/DomainMonitor.vue b/src/components/Widgets/DomainMonitor.vue index 90d6c8ae0a..274d84fcc7 100644 --- a/src/components/Widgets/DomainMonitor.vue +++ b/src/components/Widgets/DomainMonitor.vue @@ -63,11 +63,11 @@ export default { computed: { apiKey() { if (!this.options.apiKey) this.error('Missing API Key'); - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, domain() { if (!this.options.domain) this.error('Missing Domain Name Key'); - return this.options.domain; + return this.parseAsEnvVar(this.options.domain); }, endpoint() { return `${widgetApiEndpoints.domainMonitor}/?domain=${this.domain}&r=whois&apikey=${this.apiKey}`; diff --git a/src/components/Widgets/DroneCi.vue b/src/components/Widgets/DroneCi.vue index 8ffb4e84c7..b80a792db1 100644 --- a/src/components/Widgets/DroneCi.vue +++ b/src/components/Widgets/DroneCi.vue @@ -106,7 +106,7 @@ export default { if (!this.options.apiKey) { this.error('An API key is required, please see the docs for more info'); } - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, }, methods: { diff --git a/src/components/Widgets/ExchangeRates.vue b/src/components/Widgets/ExchangeRates.vue index 1c68dc14d3..294747a368 100644 --- a/src/components/Widgets/ExchangeRates.vue +++ b/src/components/Widgets/ExchangeRates.vue @@ -45,7 +45,7 @@ export default { computed: { /* The users API key for exchangerate-api.com */ apiKey() { - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, /* The currency to convert results into */ inputCurrency() { diff --git a/src/components/Widgets/Flights.vue b/src/components/Widgets/Flights.vue index 40adc8572c..f292317f2b 100644 --- a/src/components/Widgets/Flights.vue +++ b/src/components/Widgets/Flights.vue @@ -71,7 +71,7 @@ export default { this.error('An API key must be supplied'); return ''; } - return usersChoice; + return this.parseAsEnvVar(usersChoice); }, /* The direction of flights: Arrival, Departure or Both */ direction() { diff --git a/src/components/Widgets/GluetunStatus.vue b/src/components/Widgets/GluetunStatus.vue index c07e96e21c..f178dffe32 100644 --- a/src/components/Widgets/GluetunStatus.vue +++ b/src/components/Widgets/GluetunStatus.vue @@ -58,7 +58,7 @@ export default { }, hostname() { if (!this.options.hostname) this.error('`hostname` is required'); - return this.options.hostname; + return this.parseAsEnvVar(this.options.hostname); }, }, methods: { diff --git a/src/components/Widgets/HealthChecks.vue b/src/components/Widgets/HealthChecks.vue index 2ef028237a..964fbff7d1 100644 --- a/src/components/Widgets/HealthChecks.vue +++ b/src/components/Widgets/HealthChecks.vue @@ -56,7 +56,7 @@ export default { this.error('An API key is required, please see the docs for more info'); } if (typeof this.options.apiKey === 'string') { - return [this.options.apiKey]; + return [this.parseAsEnvVar(this.options.apiKey)]; } return this.options.apiKey; }, diff --git a/src/components/Widgets/Linkding.vue b/src/components/Widgets/Linkding.vue index 188fe9f2e6..0c2e26eca3 100644 --- a/src/components/Widgets/Linkding.vue +++ b/src/components/Widgets/Linkding.vue @@ -30,11 +30,11 @@ export default { computed: { endpoint() { if (!this.options.host) this.error('linkgding Host is required'); - return `${this.options.host}/api/bookmarks`; + return `${this.parseAsEnvVar(this.options.host)}/api/bookmarks`; }, apiKey() { if (!this.options.apiKey) this.error('linkgding apiKey is required'); - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, filtertags() { return this.options.tags; diff --git a/src/components/Widgets/NewsHeadlines.vue b/src/components/Widgets/NewsHeadlines.vue index d467c70c3a..9d6df0199b 100644 --- a/src/components/Widgets/NewsHeadlines.vue +++ b/src/components/Widgets/NewsHeadlines.vue @@ -29,7 +29,7 @@ export default { computed: { apiKey() { if (!this.options.apiKey) this.error('An API key is required, see docs for more info'); - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, country() { return this.options.country ? `&country=${this.options.country}` : ''; diff --git a/src/components/Widgets/NextcloudNotifications.vue b/src/components/Widgets/NextcloudNotifications.vue index dd1d4f3455..62802f2ec1 100644 --- a/src/components/Widgets/NextcloudNotifications.vue +++ b/src/components/Widgets/NextcloudNotifications.vue @@ -22,7 +22,7 @@ {{ tt('delete-notification') }} + class="action secondary">{{ tt('delete-notification') }}

diff --git a/src/components/Widgets/NextcloudStats.vue b/src/components/Widgets/NextcloudStats.vue index 8860434ff9..12e5d1feb1 100644 --- a/src/components/Widgets/NextcloudStats.vue +++ b/src/components/Widgets/NextcloudStats.vue @@ -44,7 +44,7 @@ {{ tt('local') }} {{ tt('and') }} + + shares.num_fed_shares_received)"> {{ tt('federated-shares') }} diff --git a/src/components/Widgets/PiHoleStats.vue b/src/components/Widgets/PiHoleStats.vue index ff909b7d52..fb5aa56d82 100644 --- a/src/components/Widgets/PiHoleStats.vue +++ b/src/components/Widgets/PiHoleStats.vue @@ -36,13 +36,14 @@ export default { computed: { /* Let user select which comic to display: random, latest or a specific number */ hostname() { - const usersChoice = this.options.hostname; + const usersChoice = this.parseAsEnvVar(this.options.hostname); if (!usersChoice) this.error('You must specify the hostname for your Pi-Hole server'); return usersChoice || 'http://pi.hole'; }, apiKey() { - if (!this.options.apiKey) this.error('API Key is required, please see the docs'); - return this.options.apiKey; + const usersChoice = this.parseAsEnvVar(this.options.apiKey); + if (!usersChoice) this.error('API Key is required, please see the docs'); + return usersChoice; }, endpoint() { return `${this.hostname}/admin/api.php?summary&auth=${this.apiKey}`; diff --git a/src/components/Widgets/Proxmox.vue b/src/components/Widgets/Proxmox.vue index 5ae09fcc3d..c24724afca 100644 --- a/src/components/Widgets/Proxmox.vue +++ b/src/components/Widgets/Proxmox.vue @@ -34,22 +34,22 @@ export default { computed: { clusterUrl() { if (!this.options.cluster_url) this.error('The cluster URL is required.'); - return this.options.cluster_url || ''; + return this.parseAsEnvVar(this.options.cluster_url) || ''; }, userName() { if (!this.options.user_name) this.error('The user name is required.'); - return this.options.user_name || ''; + return this.parseAsEnvVar(this.options.user_name) || ''; }, tokenName() { if (!this.options.token_name) this.error('The token name is required.'); - return this.options.token_name || ''; + return this.parseAsEnvVar(this.options.token_name) || ''; }, tokenUuid() { if (!this.options.token_uuid) this.error('The token uuid is required.'); - return this.options.token_uuid || ''; + return this.parseAsEnvVar(this.options.token_uuid) || ''; }, node() { - return this.options.node || ''; + return this.parseAsEnvVar(this.options.node) || ''; }, nodeData() { return this.options.node_data || false; diff --git a/src/components/Widgets/PublicIp.vue b/src/components/Widgets/PublicIp.vue index 86cd00327a..8a188974a3 100644 --- a/src/components/Widgets/PublicIp.vue +++ b/src/components/Widgets/PublicIp.vue @@ -35,7 +35,7 @@ export default { }, provider() { // Can be either `ip-api`, `ipapi.co` or `ipgeolocation` - return this.options.provider || 'ipapi.co'; + return this.parseAsEnvVar(this.options.provider) || 'ipapi.co'; }, }, data() { diff --git a/src/components/Widgets/RssFeed.vue b/src/components/Widgets/RssFeed.vue index de9857aa75..f958f603ad 100644 --- a/src/components/Widgets/RssFeed.vue +++ b/src/components/Widgets/RssFeed.vue @@ -51,7 +51,7 @@ export default { return this.options.rssUrl || ''; }, apiKey() { - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, parseLocally() { return this.options.parseLocally; diff --git a/src/components/Widgets/SportsScores.vue b/src/components/Widgets/SportsScores.vue index 2046e01c89..6eadc536b2 100644 --- a/src/components/Widgets/SportsScores.vue +++ b/src/components/Widgets/SportsScores.vue @@ -93,7 +93,7 @@ export default { return this.options.leagueId; }, apiKey() { - return this.options.apiKey || '50130162'; + return this.parseAsEnvVar(this.options.apiKey) || '50130162'; }, limit() { return this.options.limit || 20; diff --git a/src/components/Widgets/StockPriceChart.vue b/src/components/Widgets/StockPriceChart.vue index 68eec18667..d8fb35bfe4 100644 --- a/src/components/Widgets/StockPriceChart.vue +++ b/src/components/Widgets/StockPriceChart.vue @@ -29,7 +29,7 @@ export default { }, /* The users API key for AlphaVantage */ apiKey() { - return this.options.apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, /* The formatted GET request API endpoint to fetch stock data from */ endpoint() { diff --git a/src/components/Widgets/SynologyDownload.vue b/src/components/Widgets/SynologyDownload.vue index aa82089e15..fe2fcaa929 100644 --- a/src/components/Widgets/SynologyDownload.vue +++ b/src/components/Widgets/SynologyDownload.vue @@ -45,15 +45,15 @@ export default { computed: { hostname() { if (!this.options.hostname) this.error('A hostname is required'); - return this.options.hostname; + return this.parseAsEnvVar(this.options.hostname); }, username() { if (!this.options.username) this.error('A username is required'); - return this.options.username; + return this.parseAsEnvVar(this.options.username); }, password() { if (!this.options.password) this.error('A password is required'); - return this.options.password; + return this.parseAsEnvVar(this.options.password); }, endpointLogin() { return `${this.hostname}/webapi/auth.cgi?api=SYNO.API.Auth&version=3&method=login&account=${this.username}&passwd=${this.password}&session=DownloadStation&format=sid`; diff --git a/src/components/Widgets/UptimeKuma.vue b/src/components/Widgets/UptimeKuma.vue index b58c1d087b..b342b76702 100644 --- a/src/components/Widgets/UptimeKuma.vue +++ b/src/components/Widgets/UptimeKuma.vue @@ -52,15 +52,11 @@ export default { computed: { /* Get API key for access to instance */ apiKey() { - const { apiKey } = this.options; - - return apiKey; + return this.parseAsEnvVar(this.options.apiKey); }, /* Get instance URL */ url() { - const { url } = this.options; - - return url; + return this.parseAsEnvVar(this.options.url); }, /* Create authorisation header for the instance from the apiKey */ authHeaders() { diff --git a/src/components/Widgets/WalletBalance.vue b/src/components/Widgets/WalletBalance.vue index 923ae8f95c..c2d6dc7470 100644 --- a/src/components/Widgets/WalletBalance.vue +++ b/src/components/Widgets/WalletBalance.vue @@ -53,7 +53,7 @@ export default { }, address() { if (!this.options.address) this.error('You must specify a public address'); - return this.options.address; + return this.parseAsEnvVar(this.options.address); }, network() { return this.options.network || 'main'; diff --git a/src/components/Widgets/Weather.vue b/src/components/Widgets/Weather.vue index a72a0dded1..11b164dde5 100644 --- a/src/components/Widgets/Weather.vue +++ b/src/components/Widgets/Weather.vue @@ -46,13 +46,12 @@ export default { return this.options.units || 'metric'; }, endpoint() { - const { - apiKey, city, lat, lon, - } = this.options; - if (lat && lon) { - return `${widgetApiEndpoints.weather}?lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}`; - } - return `${widgetApiEndpoints.weather}?q=${city}&appid=${apiKey}&units=${this.units}`; + const apiKey = this.parseAsEnvVar(this.options.apiKey); + const { city, lat, lon } = this.options; + const params = (lat && lon) + ? `lat=${lat}&lon=${lon}&appid=${apiKey}&units=${this.units}` + : `q=${city}&appid=${apiKey}&units=${this.units}`; + return `${widgetApiEndpoints.weather}?${params}`; }, tempDisplayUnits() { switch (this.units) { diff --git a/src/components/Widgets/XkcdComic.vue b/src/components/Widgets/XkcdComic.vue index 2aed03cfbd..22bcc10f9d 100644 --- a/src/components/Widgets/XkcdComic.vue +++ b/src/components/Widgets/XkcdComic.vue @@ -8,7 +8,6 @@