mirror of
https://github.com/Lissy93/dashy.git
synced 2025-07-23 13:45:33 +02:00
🔀 Merge pull request #1542 from Lissy93/FEAT/3.0.1-improvements
[FEAT] Clearer error messaging and documented user-data dir (3.0.1)
This commit is contained in:
commit
d92ae25700
13
.env
13
.env
@ -27,6 +27,17 @@
|
|||||||
# The path to the user data directory
|
# The path to the user data directory
|
||||||
# USER_DATA_DIR=user-data
|
# USER_DATA_DIR=user-data
|
||||||
|
|
||||||
|
# Enable HTTP basic auth to protect your *.yml config files
|
||||||
|
# ENABLE_HTTP_AUTH=true
|
||||||
|
|
||||||
|
# Enable basic HTTP auth to protect your *.yml config files
|
||||||
|
# BASIC_AUTH_USERNAME
|
||||||
|
# BASIC_AUTH_PASSWORD
|
||||||
|
|
||||||
|
# If you'd like frontend to automatically authenticate when basic auth enabled, set credentials here too
|
||||||
|
# VUE_APP_BASIC_AUTH_USERNAME
|
||||||
|
# VUE_APP_BASIC_AUTH_PASSWORD
|
||||||
|
|
||||||
# Override where the path to the configuration file is, can be a remote URL
|
# Override where the path to the configuration file is, can be a remote URL
|
||||||
# VUE_APP_CONFIG_PATH=/conf.yml
|
# VUE_APP_CONFIG_PATH=/conf.yml
|
||||||
|
|
||||||
@ -52,7 +63,7 @@
|
|||||||
# VUE_APP_VERSION=2.0.0
|
# VUE_APP_VERSION=2.0.0
|
||||||
|
|
||||||
# Directory for conf.yml backups
|
# Directory for conf.yml backups
|
||||||
# BACKUP_DIR=./user-data/
|
# BACKUP_DIR=./user-data/config-backups
|
||||||
|
|
||||||
# Setup any other user defined vars by prepending VUE_APP_ to the var name
|
# 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_ip=http://your.pihole.ip
|
||||||
|
22
.github/workflows/new-issues-check.yml
vendored
22
.github/workflows/new-issues-check.yml
vendored
@ -1,22 +0,0 @@
|
|||||||
name: ⭐ Hello non-Stargazers
|
|
||||||
on:
|
|
||||||
issues:
|
|
||||||
types: [opened, reopened]
|
|
||||||
jobs:
|
|
||||||
check-user:
|
|
||||||
if: >
|
|
||||||
${{
|
|
||||||
! contains( github.event.issue.labels.*.name, '📌 Keep Open') &&
|
|
||||||
! contains( github.event.issue.labels.*.name, '🌈 Feedback') &&
|
|
||||||
! contains( github.event.issue.labels.*.name, '💯 Showcase') &&
|
|
||||||
github.event.comment.author_association != 'CONTRIBUTOR'
|
|
||||||
}}
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
name: Add comment to issues opened by non-stargazers
|
|
||||||
steps:
|
|
||||||
- name: comment
|
|
||||||
uses: qxip/please-star-light@v4
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.BOT_GITHUB_TOKEN || secrets.GITHUB_TOKEN }}
|
|
||||||
autoclose: false
|
|
||||||
message: "If you're enjoying Dashy, consider dropping us a ⭐<br>_<sub>🤖 I'm a bot, and this message was automated</sub>_"
|
|
@ -42,7 +42,7 @@ RUN apk add --no-cache tzdata
|
|||||||
COPY --from=BUILD_IMAGE /app ./
|
COPY --from=BUILD_IMAGE /app ./
|
||||||
|
|
||||||
# Finally, run start command to serve up the built application
|
# Finally, run start command to serve up the built application
|
||||||
CMD [ "yarn", "build-and-start" ]
|
CMD [ "yarn", "start" ]
|
||||||
|
|
||||||
# Expose the port
|
# Expose the port
|
||||||
EXPOSE ${PORT}
|
EXPOSE ${PORT}
|
||||||
|
@ -8,6 +8,14 @@
|
|||||||
<b><a href="./docs/showcase.md">User Showcase</a></b> | <b><a href="https://demo.dashy.to">Live Demo</a></b> | <b><a href="./docs/quick-start.md">Getting Started</a></b> | <b><a href="https://dashy.to/docs">Documentation</a></b> | <b><a href="https://github.com/Lissy93/dashy">GitHub</a></b>
|
<b><a href="./docs/showcase.md">User Showcase</a></b> | <b><a href="https://demo.dashy.to">Live Demo</a></b> | <b><a href="./docs/quick-start.md">Getting Started</a></b> | <b><a href="https://dashy.to/docs">Documentation</a></b> | <b><a href="https://github.com/Lissy93/dashy">GitHub</a></b>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
|
<p align="center">
|
||||||
|
<br>
|
||||||
|
<sup>Dashy is kindly sponsored by <a href="https://umbrel.com?ref=dashy">Umbrel</a> - the personal home cloud and OS for self-hosting</sup><br>
|
||||||
|
<a href="https://umbrel.com?ref=dashy">
|
||||||
|
<img width="400" src="https://github.com/Lissy93/dashy/blob/WEBSITE/docs-site-source/static/umbrel-banner.jpg?raw=true" />
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
|
||||||
> [!NOTE]
|
> [!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.
|
> 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.
|
||||||
|
|
||||||
|
@ -20,7 +20,7 @@ services:
|
|||||||
# - /path/to/my-config.yml:/app/user-data/conf.yml
|
# - /path/to/my-config.yml:/app/user-data/conf.yml
|
||||||
# - /path/to/item-icons:/app/user-data/item-icons/
|
# - /path/to/item-icons:/app/user-data/item-icons/
|
||||||
|
|
||||||
# Set port that web service will be served on. Keep container port as 80
|
# Set port that web service will be served on. Keep container port as 8080
|
||||||
ports:
|
ports:
|
||||||
- 4000:8080
|
- 4000:8080
|
||||||
|
|
||||||
|
@ -6,7 +6,10 @@
|
|||||||
- [Logging In and Out](#logging-in-and-out)
|
- [Logging In and Out](#logging-in-and-out)
|
||||||
- [Guest Access](#enabling-guest-access)
|
- [Guest Access](#enabling-guest-access)
|
||||||
- [Per-User Access](#granular-access)
|
- [Per-User Access](#granular-access)
|
||||||
|
- [Using Environment Variables for Passwords](#using-environment-variables-for-passwords)
|
||||||
|
- [Adding HTTP Auth to Configuration](#adding-http-auth-to-configuration)
|
||||||
- [Security Considerations](#security)
|
- [Security Considerations](#security)
|
||||||
|
- [HTTP Auth](#http-auth)
|
||||||
- [Keycloak Auth](#keycloak)
|
- [Keycloak Auth](#keycloak)
|
||||||
- [Deploying Keycloak](#1-deploy-keycloak)
|
- [Deploying Keycloak](#1-deploy-keycloak)
|
||||||
- [Setting up Keycloak](#2-setup-keycloak-users)
|
- [Setting up Keycloak](#2-setup-keycloak-users)
|
||||||
@ -115,6 +118,27 @@ You can also prevent any user from writing changes to disk, using `preventWriteT
|
|||||||
|
|
||||||
To disable all UI config features, including View Config, set `disableConfiguration`. Alternatively you can disable UI config features for all non admin users by setting `disableConfigurationForNonAdmin` to true.
|
To disable all UI config features, including View Config, set `disableConfiguration`. Alternatively you can disable UI config features for all non admin users by setting `disableConfigurationForNonAdmin` to true.
|
||||||
|
|
||||||
|
### Using Environment Variables for Passwords
|
||||||
|
|
||||||
|
If you don't want to hash your password, you can instead leave out the `hash` attribute, and replace it with `password` which should have the value of an environmental variable name you wish to use.
|
||||||
|
|
||||||
|
Note that env var must begin with `VUE_APP_`, and you must set this variable before building the app.
|
||||||
|
|
||||||
|
For example:
|
||||||
|
|
||||||
|
```yaml
|
||||||
|
auth:
|
||||||
|
users:
|
||||||
|
- user: bob
|
||||||
|
password: VUE_APP_BOB
|
||||||
|
```
|
||||||
|
|
||||||
|
Just be sure to set `VUE_APP_BOB='my super secret password'` before build-time.
|
||||||
|
|
||||||
|
### Adding HTTP Auth to Configuration
|
||||||
|
|
||||||
|
If you'd also like to prevent direct visit access to your configuration file, you can set the `ENABLE_HTTP_AUTH` environmental variable.
|
||||||
|
|
||||||
### Security
|
### Security
|
||||||
|
|
||||||
With basic auth, all logic is happening on the client-side, which could mean a skilled user could manipulate the code to view parts of your configuration, including the hash. If the SHA-256 hash is of a common password, it may be possible to determine it, using a lookup table, in order to find the original password. Which can be used to manually generate the auth token, that can then be inserted into session storage, to become a valid logged in user. Therefore, you should always use a long, strong and unique password, and if you instance contains security-critical info and/ or is exposed directly to the internet, and alternative authentication method may be better. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage.
|
With basic auth, all logic is happening on the client-side, which could mean a skilled user could manipulate the code to view parts of your configuration, including the hash. If the SHA-256 hash is of a common password, it may be possible to determine it, using a lookup table, in order to find the original password. Which can be used to manually generate the auth token, that can then be inserted into session storage, to become a valid logged in user. Therefore, you should always use a long, strong and unique password, and if you instance contains security-critical info and/ or is exposed directly to the internet, and alternative authentication method may be better. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage.
|
||||||
@ -123,6 +147,16 @@ With basic auth, all logic is happening on the client-side, which could mean a s
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
## HTTP Auth
|
||||||
|
|
||||||
|
If you'd like to protect all your config files from direct access, you can set the `BASIC_AUTH_USERNAME` and `BASIC_AUTH_PASSWORD` environmental variables. You'll then be prompted to enter these credentials when visiting Dashy.
|
||||||
|
|
||||||
|
Then, if you'd like your frontend to automatically log you in, without prompting you for credentials, then also specify `VUE_APP_BASIC_AUTH_USERNAME` and `VUE_APP_BASIC_AUTH_PASSWORD`. This is useful for when you're hosting Dashy on a private server, and you want to prevent unauthorized access to your config files, while still allowing the frontend to access them. Note that a rebuild is required for these changes to take effect.
|
||||||
|
|
||||||
|
**[⬆️ Back to Top](#authentication)**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
## Keycloak
|
## Keycloak
|
||||||
|
|
||||||
Dashy also supports using a [Keycloak](https://www.keycloak.org/) authentication server. The setup for this is a bit more involved, but it gives you greater security overall, useful for if your instance is exposed to the internet.
|
Dashy also supports using a [Keycloak](https://www.keycloak.org/) authentication server. The setup for this is a bit more involved, but it gives you greater security overall, useful for if your instance is exposed to the internet.
|
||||||
|
@ -32,7 +32,32 @@ Your dashboard should now be up and running at `http://localhost:8080` (or your
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 3. Configure
|
## 3. User Data Directory
|
||||||
|
|
||||||
|
Your config file should be placed inside `user-data/` (in Docker, that's `/app/user-data/`).
|
||||||
|
|
||||||
|
This directory can also contain some optional assets you wish to use within your dashboard, like icons, fonts, styles, scripts, etc.
|
||||||
|
|
||||||
|
Any files placed here will be served up to the root of the domain, and override the contents of `public/`.
|
||||||
|
For example, if you had `user-data/favicon.ico` this would be accessible at `http://my-dashy-instance.local/favicon.ico`
|
||||||
|
|
||||||
|
Example Files in `user-data`:
|
||||||
|
- `conf.yml` - This is the only file that is compulsary, it's your main Dashy config
|
||||||
|
- `**.yml` - Include more config files, if you'd like to have multiple pages, see [Multi-page support](/docs/pages-and-sections.md#multi-page-support) for docs
|
||||||
|
- `favicon.ico` - The default favicon, shown in the browser's tab title
|
||||||
|
- `initialization.html` - Static HTML page displayed before the app has finished compiling, see [`public/initialization.html`](https://github.com/Lissy93/dashy/blob/master/public/initialization.html)
|
||||||
|
- `robots.txt` - Search engine crawl rules, override this if you want your dashboard to be indexable
|
||||||
|
- `manifest.json` - PWA configuration file, for installing Dashy on mobile devices
|
||||||
|
- `index.html` - The main index page which initializes the client-side app, copy it from [`/public/index.html`](https://github.com/Lissy93/dashy/blob/master/public/index.html)
|
||||||
|
- `**.html` - Write your own HTML pages, and access them at `http://my-dashy-instance.local/my-page.html`
|
||||||
|
- `fonts/` - Custom fonts (be sure to include the ones already in [`public/fonts`](https://github.com/Lissy93/dashy/tree/master/public/fonts)
|
||||||
|
- `item-icons/` - To use your own icons for items on your dashboard, see [Icons --> Local Icons](/docs/icons.md#local-icons)
|
||||||
|
- `web-icons/` - Override Dashy logo
|
||||||
|
- `widget-resources/` - Fonts, icons and assets for custom widgets
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Configure
|
||||||
|
|
||||||
Now that you've got Dashy running, you are going to want to set it up with your own content.
|
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 [`/user-data/conf.yml`](https://github.com/Lissy93/dashy/blob/master/user-data/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).
|
||||||
@ -41,6 +66,7 @@ The format on the config file is pretty straight forward. There are three root a
|
|||||||
- [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo) - Dashboard meta data, like title, description, nav bar links and footer text
|
- [`pageInfo`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pageinfo) - Dashboard meta data, like title, description, nav bar links and footer text
|
||||||
- [`appConfig`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#appconfig-optional) - Dashboard settings, like themes, authentication, language and customization
|
- [`appConfig`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#appconfig-optional) - Dashboard settings, like themes, authentication, language and customization
|
||||||
- [`sections`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#section) - An array of sections, each including an array of items
|
- [`sections`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#section) - An array of sections, each including an array of items
|
||||||
|
- [`pages`](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md#pages-optional) - Have multiples pages in your dashboard
|
||||||
|
|
||||||
You can view a full list of all available config options in the [Configuring Docs](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md).
|
You can view a full list of all available config options in the [Configuring Docs](https://github.com/Lissy93/dashy/blob/master/docs/configuring.md).
|
||||||
|
|
||||||
@ -76,11 +102,11 @@ Notes:
|
|||||||
- It's also possible to edit your config directly through the UI, and changes will be saved in this file
|
- 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`
|
- 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)
|
- You might find it helpful to look at some examples, a collection of which can be [found here](https://gist.github.com/Lissy93/000f712a5ce98f212817d20bc16bab10)
|
||||||
- After editing your config, the app will rebuild in the background, which may take a minute
|
- It's also possible to load a remote config, e.g. from a GitHub Gist
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 4. Further Customisation
|
## 5. Further Customisation
|
||||||
|
|
||||||
Once you've got Dashy setup, you'll want to ensure the container is properly healthy, secured, backed up and kept up-to-date. All this is covered in the [Management Docs](https://github.com/Lissy93/dashy/blob/master/docs/management.md).
|
Once you've got Dashy setup, you'll want to ensure the container is properly healthy, secured, backed up and kept up-to-date. All this is covered in the [Management Docs](https://github.com/Lissy93/dashy/blob/master/docs/management.md).
|
||||||
|
|
||||||
@ -97,7 +123,7 @@ You might also want to check out the docs for specific features you'd like to us
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## 5. Final Note
|
## 6. Final Note
|
||||||
|
|
||||||
If you need any help or support in getting Dashy running, head over to the [Discussions](https://github.com/Lissy93/dashy/discussions) page. If you think you've found a bug, please do [raise it](https://github.com/Lissy93/dashy/issues/new/choose) so it can be fixed. For contact options, see the [Support Page](https://github.com/Lissy93/dashy/blob/master/.github/SUPPORT.md).
|
If you need any help or support in getting Dashy running, head over to the [Discussions](https://github.com/Lissy93/dashy/discussions) page. If you think you've found a bug, please do [raise it](https://github.com/Lissy93/dashy/issues/new/choose) so it can be fixed. For contact options, see the [Support Page](https://github.com/Lissy93/dashy/blob/master/.github/SUPPORT.md).
|
||||||
|
|
||||||
@ -118,7 +144,7 @@ yarn build # Build the app
|
|||||||
yarn start # Start the app
|
yarn start # Start the app
|
||||||
```
|
```
|
||||||
|
|
||||||
Then edit `./user-data/conf.yml` and rebuild the app with `yarn build`
|
Then edit `./user-data/conf.yml`
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "dashy",
|
"name": "dashy",
|
||||||
"version": "3.0.0",
|
"version": "3.0.1",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"main": "server",
|
"main": "server",
|
||||||
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
|
||||||
@ -26,6 +26,7 @@
|
|||||||
"connect-history-api-fallback": "^1.6.0",
|
"connect-history-api-fallback": "^1.6.0",
|
||||||
"crypto-js": "^4.2.0",
|
"crypto-js": "^4.2.0",
|
||||||
"express": "^4.17.2",
|
"express": "^4.17.2",
|
||||||
|
"express-basic-auth": "^1.2.1",
|
||||||
"frappe-charts": "^1.6.2",
|
"frappe-charts": "^1.6.2",
|
||||||
"js-yaml": "^4.1.0",
|
"js-yaml": "^4.1.0",
|
||||||
"keycloak-js": "^20.0.3",
|
"keycloak-js": "^20.0.3",
|
||||||
|
Binary file not shown.
Before Width: | Height: | Size: 45 KiB |
Binary file not shown.
Before Width: | Height: | Size: 45 KiB |
Binary file not shown.
Before Width: | Height: | Size: 827 B |
@ -50,6 +50,14 @@
|
|||||||
|
|
||||||
<p class="time-note" id="note">This may take a minute or two</p>
|
<p class="time-note" id="note">This may take a minute or two</p>
|
||||||
|
|
||||||
|
<div class="why-am-i-seeing-this">
|
||||||
|
<h3>Why are you seeing this screen?</h3>
|
||||||
|
<p>
|
||||||
|
The app's built files aren't yet present in the /dist directory,
|
||||||
|
so this page is displayed while we compile the source.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
<style lang="css">
|
<style lang="css">
|
||||||
/* Page Layout Styles */
|
/* Page Layout Styles */
|
||||||
body,
|
body,
|
||||||
@ -60,7 +68,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
body {
|
body {
|
||||||
background: #141b33;
|
background: #0d1220;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
@ -194,15 +202,34 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.hide { display: none; }
|
.hide { display: none; }
|
||||||
|
|
||||||
|
.why-am-i-seeing-this {
|
||||||
|
color: #808080a6;
|
||||||
|
font-family: Tahoma, 'Trebuchet MS', sans-serif;
|
||||||
|
max-width: 25rem;
|
||||||
|
border: 2px solid #808080a6;
|
||||||
|
border-radius: 6px;
|
||||||
|
padding: 0.5rem;
|
||||||
|
position: absolute;
|
||||||
|
bottom: 1rem;
|
||||||
|
background: #8080800d;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
}
|
||||||
|
.why-am-i-seeing-this h3 {
|
||||||
|
margin: 0 0 0.5rem 0;
|
||||||
|
}
|
||||||
|
.why-am-i-seeing-this p {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
const refreshRate = 8000;
|
// Refresh the page every 10 seconds
|
||||||
// Refresh at interval
|
const refreshRate = 10000;
|
||||||
setTimeout(() => { location.reload(); }, refreshRate);
|
setTimeout(() => { location.reload(); }, refreshRate);
|
||||||
|
|
||||||
// Get current stage
|
// Get current stage
|
||||||
let initStage = parseInt(sessionStorage.getItem('initStage') || 0);
|
let initStage = parseInt(sessionStorage.getItem('initStage') || 0);
|
||||||
|
|
||||||
// Check if stage in session storage is old, and if so, reset it
|
// Check if stage in session storage is old, and if so, reset it
|
||||||
const now = Math.round(Date.now()/1000);
|
const now = Math.round(Date.now()/1000);
|
||||||
@ -262,4 +289,4 @@
|
|||||||
</script>
|
</script>
|
||||||
</body>
|
</body>
|
||||||
|
|
||||||
</html>
|
</html>
|
||||||
|
75
server.js
75
server.js
@ -6,14 +6,20 @@
|
|||||||
* */
|
* */
|
||||||
|
|
||||||
/* Import built-in Node server modules */
|
/* Import built-in Node server modules */
|
||||||
|
const fs = require('fs');
|
||||||
|
const os = require('os');
|
||||||
|
const dns = require('dns');
|
||||||
const http = require('http');
|
const http = require('http');
|
||||||
const path = require('path');
|
const path = require('path');
|
||||||
const util = require('util');
|
const util = require('util');
|
||||||
const dns = require('dns');
|
const crypto = require('crypto');
|
||||||
const os = require('os');
|
|
||||||
|
/* Import NPM dependencies */
|
||||||
|
const yaml = require('js-yaml');
|
||||||
|
|
||||||
/* Import Express + middleware functions */
|
/* Import Express + middleware functions */
|
||||||
const express = require('express');
|
const express = require('express');
|
||||||
|
const basicAuth = require('express-basic-auth');
|
||||||
const history = require('connect-history-api-fallback');
|
const history = require('connect-history-api-fallback');
|
||||||
|
|
||||||
/* Kick of some basic checks */
|
/* Kick of some basic checks */
|
||||||
@ -61,7 +67,7 @@ const printWelcomeMessage = () => {
|
|||||||
console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console
|
console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// Fetching info for welcome message failed, print simple msg instead
|
// No clue what could of gone wrong here, but print fallback message if above failed
|
||||||
console.log(`Dashy server has started (${port})`); // eslint-disable-line no-console
|
console.log(`Dashy server has started (${port})`); // eslint-disable-line no-console
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@ -71,6 +77,64 @@ const printWarning = (msg, error) => {
|
|||||||
console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console
|
console.warn(`\x1b[103m\x1b[34m${msg}\x1b[0m\n`, error || ''); // eslint-disable-line no-console
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/* Load appConfig.auth.users from config (if present) for authorization purposes */
|
||||||
|
function loadUserConfig() {
|
||||||
|
try {
|
||||||
|
const filePath = path.join(__dirname, process.env.USER_DATA_DIR || 'user-data', 'conf.yml');
|
||||||
|
const fileContents = fs.readFileSync(filePath, 'utf8');
|
||||||
|
const data = yaml.load(fileContents);
|
||||||
|
return data?.appConfig?.auth?.users || null;
|
||||||
|
} catch (e) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If HTTP auth is enabled, and no username/password are pre-set, then check passed credentials */
|
||||||
|
function customAuthorizer(username, password) {
|
||||||
|
const sha256 = (input) => crypto.createHash('sha256').update(input).digest('hex').toUpperCase();
|
||||||
|
const generateUserToken = (user) => {
|
||||||
|
if (!user.user || (!user.hash && !user.password)) return '';
|
||||||
|
const strAndUpper = (input) => input.toString().toUpperCase();
|
||||||
|
const passwordHash = user.hash || sha256(process.env[user.password]);
|
||||||
|
const sha = sha256(strAndUpper(user.user) + strAndUpper(passwordHash));
|
||||||
|
return strAndUpper(sha);
|
||||||
|
};
|
||||||
|
if (password.startsWith('Bearer ')) {
|
||||||
|
const token = password.slice('Bearer '.length);
|
||||||
|
const users = loadUserConfig();
|
||||||
|
return users.some(user => generateUserToken(user) === token);
|
||||||
|
} else {
|
||||||
|
const users = loadUserConfig();
|
||||||
|
const userHash = sha256(password);
|
||||||
|
return users.some(user => (
|
||||||
|
user.user.toLowerCase() === username.toLowerCase() && user.hash.toUpperCase() === userHash
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* If a username and password are set, setup auth for config access, otherwise skip */
|
||||||
|
function getBasicAuthMiddleware() {
|
||||||
|
const configUsers = process.env.ENABLE_HTTP_AUTH ? loadUserConfig() : null;
|
||||||
|
const { BASIC_AUTH_USERNAME, BASIC_AUTH_PASSWORD } = process.env;
|
||||||
|
if (BASIC_AUTH_USERNAME && BASIC_AUTH_PASSWORD) {
|
||||||
|
return basicAuth({
|
||||||
|
users: { [BASIC_AUTH_USERNAME]: BASIC_AUTH_PASSWORD },
|
||||||
|
challenge: true,
|
||||||
|
unauthorizedResponse: () => 'Unauthorized - Incorrect username or password',
|
||||||
|
});
|
||||||
|
} else if ((configUsers && configUsers.length > 0)) {
|
||||||
|
return basicAuth({
|
||||||
|
authorizer: customAuthorizer,
|
||||||
|
challenge: true,
|
||||||
|
unauthorizedResponse: () => 'Unauthorized - Incorrect token',
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return (req, res, next) => next();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const protectConfig = getBasicAuthMiddleware();
|
||||||
|
|
||||||
/* A middleware function for Connect, that filters requests based on method type */
|
/* A middleware function for Connect, that filters requests based on method type */
|
||||||
const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next());
|
const method = (m, mw) => (req, res, next) => (req.method === m ? mw(req, res, next) : next());
|
||||||
|
|
||||||
@ -134,6 +198,11 @@ const app = express()
|
|||||||
res.end(JSON.stringify({ success: false, message: e }));
|
res.end(JSON.stringify({ success: false, message: e }));
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
// Middleware to serve any .yml files in USER_DATA_DIR with optional protection
|
||||||
|
.get('/*.yml', protectConfig, (req, res) => {
|
||||||
|
const ymlFile = req.path.split('/').pop();
|
||||||
|
res.sendFile(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data', ymlFile));
|
||||||
|
})
|
||||||
// Serves up static files
|
// Serves up static files
|
||||||
.use(express.static(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data')))
|
.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, 'dist')))
|
||||||
|
@ -11,7 +11,7 @@ const schema = require('../src/utils/ConfigSchema.json');
|
|||||||
|
|
||||||
/* Tell AJV to use strict mode, and report all errors */
|
/* Tell AJV to use strict mode, and report all errors */
|
||||||
const validatorOptions = {
|
const validatorOptions = {
|
||||||
strict: true,
|
strict: false,
|
||||||
allowUnionTypes: true,
|
allowUnionTypes: true,
|
||||||
allErrors: true,
|
allErrors: true,
|
||||||
};
|
};
|
||||||
|
@ -1,15 +1,15 @@
|
|||||||
module.exports = (config, req) => {
|
module.exports = (config, req) => {
|
||||||
try {
|
try {
|
||||||
if ( config.appConfig.auth.enableHeaderAuth ) {
|
if (config.appConfig.auth.enableHeaderAuth) {
|
||||||
const userHeader = config.appConfig.auth.headerAuth.userHeader;
|
const { userHeader } = config.appConfig.auth.headerAuth;
|
||||||
const proxyWhitelist = config.appConfig.auth.headerAuth.proxyWhitelist;
|
const { proxyWhitelist } = config.appConfig.auth.headerAuth;
|
||||||
if ( proxyWhitelist.includes(req.socket.remoteAddress) ) {
|
if (proxyWhitelist.includes(req.socket.remoteAddress)) {
|
||||||
return { "success": true, "user": req.headers[userHeader.toLowerCase()] };
|
return { success: true, user: req.headers[userHeader.toLowerCase()] };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return {};
|
return {};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
console.warn("Error get-user: ", e);
|
console.warn('Error get-user: ', e);
|
||||||
return { 'success': false };
|
return { success: false };
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
@ -14,18 +14,23 @@ module.exports = async (newConfig, render) => {
|
|||||||
return configObj.filename.replaceAll('/', '').replaceAll('..', '');
|
return configObj.filename.replaceAll('/', '').replaceAll('..', '');
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Path to config file (with navigational characters stripped)
|
||||||
const usersFileName = makeSafeFileName(newConfig);
|
const usersFileName = makeSafeFileName(newConfig);
|
||||||
|
|
||||||
|
// Path to user data directory
|
||||||
|
const userDataDirectory = process.env.USER_DATA_DIR || './user-data/';
|
||||||
|
|
||||||
// Define constants for the config file
|
// Define constants for the config file
|
||||||
const settings = {
|
const settings = {
|
||||||
defaultLocation: process.env.USER_DATA_DIR || './user-data/',
|
defaultLocation: userDataDirectory,
|
||||||
|
backupLocation: process.env.BACKUP_DIR || path.join(userDataDirectory, 'config-backups'),
|
||||||
defaultFile: 'conf.yml',
|
defaultFile: 'conf.yml',
|
||||||
filename: 'conf',
|
filename: 'conf',
|
||||||
backupDenominator: '.backup.yml',
|
backupDenominator: '.backup.yml',
|
||||||
};
|
};
|
||||||
|
|
||||||
// Make the full file name and path to save the backup config file
|
// Make the full file name and path to save the backup config file
|
||||||
const backupFilePath = `${path.normalize(process.env.BACKUP_DIR || settings.defaultLocation)
|
const backupFilePath = `${path.normalize(settings.backupLocation)
|
||||||
}/${usersFileName || settings.filename}-`
|
}/${usersFileName || settings.filename}-`
|
||||||
+ `${Math.round(new Date() / 1000)}${settings.backupDenominator}`;
|
+ `${Math.round(new Date() / 1000)}${settings.backupDenominator}`;
|
||||||
|
|
||||||
@ -45,15 +50,20 @@ module.exports = async (newConfig, render) => {
|
|||||||
message: !success ? errorMsg : getSuccessMessage(),
|
message: !success ? errorMsg : getSuccessMessage(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Makes a backup of the existing config file
|
// Create a backup of current config, and if backup dir doesn't yet exist, create it
|
||||||
await fsPromises
|
await fsPromises
|
||||||
.copyFile(defaultFilePath, backupFilePath)
|
.mkdir(settings.backupLocation, { recursive: true })
|
||||||
.catch((error) => render(getRenderMessage(false, `Unable to backup ${settings.defaultFile}: ${error}`)));
|
.then(() => fsPromises.copyFile(defaultFilePath, backupFilePath))
|
||||||
|
.catch((error) => render(
|
||||||
|
getRenderMessage(false, `Unable to backup ${settings.defaultFile}: ${error}`),
|
||||||
|
));
|
||||||
|
|
||||||
// Writes the new content to the conf.yml file
|
// Writes the new content to the conf.yml file
|
||||||
await fsPromises
|
await fsPromises
|
||||||
.writeFile(defaultFilePath, newConfig.config.toString(), writeFileOptions)
|
.writeFile(defaultFilePath, newConfig.config.toString(), writeFileOptions)
|
||||||
.catch((error) => render(getRenderMessage(false, `Unable to write to ${settings.defaultFile}: ${error}`)));
|
.catch((error) => render(
|
||||||
|
getRenderMessage(false, `Unable to write to ${settings.defaultFile}: ${error}`),
|
||||||
|
));
|
||||||
|
|
||||||
// If successful, then render hasn't yet been called- call it
|
// If successful, then render hasn't yet been called- call it
|
||||||
await render(getRenderMessage(true));
|
await render(getRenderMessage(true));
|
||||||
|
@ -4,6 +4,7 @@
|
|||||||
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
|
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
|
||||||
<Header :pageInfo="pageInfo" />
|
<Header :pageInfo="pageInfo" />
|
||||||
<router-view v-if="!isFetching" />
|
<router-view v-if="!isFetching" />
|
||||||
|
<CriticalError v-if="hasCriticalError" />
|
||||||
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
|
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -12,6 +13,7 @@
|
|||||||
import Header from '@/components/PageStrcture/Header.vue';
|
import Header from '@/components/PageStrcture/Header.vue';
|
||||||
import Footer from '@/components/PageStrcture/Footer.vue';
|
import Footer from '@/components/PageStrcture/Footer.vue';
|
||||||
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
|
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
|
||||||
|
import CriticalError from '@/components/PageStrcture/CriticalError.vue';
|
||||||
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
|
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
|
||||||
import { welcomeMsg } from '@/utils/CoolConsole';
|
import { welcomeMsg } from '@/utils/CoolConsole';
|
||||||
import ErrorHandler from '@/utils/ErrorHandler';
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
@ -29,6 +31,7 @@ export default {
|
|||||||
Footer,
|
Footer,
|
||||||
LoadingScreen,
|
LoadingScreen,
|
||||||
EditModeTopBanner,
|
EditModeTopBanner,
|
||||||
|
CriticalError,
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
@ -72,6 +75,9 @@ export default {
|
|||||||
isEditMode() {
|
isEditMode() {
|
||||||
return this.$store.state.editMode;
|
return this.$store.state.editMode;
|
||||||
},
|
},
|
||||||
|
hasCriticalError() {
|
||||||
|
return this.$store.state.criticalError;
|
||||||
|
},
|
||||||
subPageClassName() {
|
subPageClassName() {
|
||||||
const currentSubPage = this.$store.state.currentConfigInfo;
|
const currentSubPage = this.$store.state.currentConfigInfo;
|
||||||
return (currentSubPage && currentSubPage.pageId) ? currentSubPage.pageId : '';
|
return (currentSubPage && currentSubPage.pageId) ? currentSubPage.pageId : '';
|
||||||
|
@ -312,6 +312,14 @@
|
|||||||
"view-title": "View Config"
|
"view-title": "View Config"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"critical-error": {
|
||||||
|
"title": "Configuration Load Error",
|
||||||
|
"subtitle": "Dashy has failed to load correctly due to a configuration error.",
|
||||||
|
"sub-ensure-that": "Ensure that",
|
||||||
|
"sub-error-details": "Error Details",
|
||||||
|
"sub-next-steps": "Next Steps",
|
||||||
|
"ignore-button": "Ignore Critical Errors"
|
||||||
|
},
|
||||||
"widgets": {
|
"widgets": {
|
||||||
"general": {
|
"general": {
|
||||||
"loading": "Loading...",
|
"loading": "Loading...",
|
||||||
|
@ -116,7 +116,8 @@ export default {
|
|||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
const jsonData = { ...this.config };
|
const jsonData = { ...this.config };
|
||||||
jsonData.sections = jsonData.sections.map(({ filteredItems, ...section }) => section);
|
jsonData.sections = (jsonData.sections || []).map(({ filteredItems, ...section }) => section);
|
||||||
|
if (!jsonData.pageInfo) jsonData.pageInfo = { title: 'Dashy' };
|
||||||
this.jsonData = jsonData;
|
this.jsonData = jsonData;
|
||||||
if (!this.allowWriteToDisk) this.saveMode = 'local';
|
if (!this.allowWriteToDisk) this.saveMode = 'local';
|
||||||
},
|
},
|
||||||
|
@ -64,7 +64,6 @@ export default {
|
|||||||
return this.$store.state.editMode;
|
return this.$store.state.editMode;
|
||||||
},
|
},
|
||||||
sectionKey() {
|
sectionKey() {
|
||||||
if (this.isEditMode) return undefined;
|
|
||||||
return `collapsible-${this.uniqueKey}`;
|
return `collapsible-${this.uniqueKey}`;
|
||||||
},
|
},
|
||||||
collapseClass() {
|
collapseClass() {
|
||||||
@ -104,12 +103,23 @@ export default {
|
|||||||
watch: {
|
watch: {
|
||||||
checkboxState(newState) {
|
checkboxState(newState) {
|
||||||
this.isExpanded = newState;
|
this.isExpanded = newState;
|
||||||
|
this.updateLocalStorage(); // Save every change immediately
|
||||||
},
|
},
|
||||||
uniqueKey() {
|
uniqueKey(newVal, oldVal) {
|
||||||
this.checkboxState = this.isExpanded;
|
if (newVal !== oldVal) {
|
||||||
|
this.refreshCollapseState(); // Refresh state when key changes
|
||||||
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
refreshCollapseState() {
|
||||||
|
this.checkboxState = this.isExpanded;
|
||||||
|
},
|
||||||
|
updateLocalStorage() {
|
||||||
|
const collapseState = this.locallyStoredCollapseStates();
|
||||||
|
collapseState[this.uniqueKey] = this.checkboxState;
|
||||||
|
localStorage.setItem(localStorageKeys.COLLAPSE_STATE, JSON.stringify(collapseState));
|
||||||
|
},
|
||||||
/* Either expand or collapse section, based on it's current state */
|
/* Either expand or collapse section, based on it's current state */
|
||||||
toggle() {
|
toggle() {
|
||||||
this.checkboxState = !this.checkboxState;
|
this.checkboxState = !this.checkboxState;
|
||||||
|
153
src/components/PageStrcture/CriticalError.vue
Normal file
153
src/components/PageStrcture/CriticalError.vue
Normal file
@ -0,0 +1,153 @@
|
|||||||
|
<template>
|
||||||
|
<div class="critical-error-wrap" v-if="shouldShow">
|
||||||
|
<button class="close" title="Close Warning" @click="close">🗙</button>
|
||||||
|
<h3>{{ $t('critical-error.title') }}</h3>
|
||||||
|
<p>{{ $t('critical-error.subtitle') }}</p>
|
||||||
|
<h4>{{ $t('critical-error.sub-ensure-that') }}</h4>
|
||||||
|
<ul>
|
||||||
|
<li>The configuration file can be found at the specified location</li>
|
||||||
|
<li>There are no CORS rules preventing client-side access</li>
|
||||||
|
<li>The YAML is valid, parsable and matches the schema</li>
|
||||||
|
</ul>
|
||||||
|
<h4>{{ $t('critical-error.sub-error-details') }}</h4>
|
||||||
|
<pre>{{ this.$store.state.criticalError }}</pre>
|
||||||
|
<h4>{{ $t('critical-error.sub-next-steps') }}</h4>
|
||||||
|
<ul>
|
||||||
|
<li>Check the browser console for more details
|
||||||
|
(<a href="https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md#how-to-open-browser-console">see how</a>)
|
||||||
|
</li>
|
||||||
|
<li>View the
|
||||||
|
<a href="https://github.com/Lissy93/dashy/blob/master/docs/troubleshooting.md">Troubleshooting Guide</a>
|
||||||
|
and <a href="https://dashy.to/docs/">Docs</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
If you've verified the config is present, accessible and valid, and cannot find the solution
|
||||||
|
in the troubleshooting, docs or GitHub issues,
|
||||||
|
then <a href="https://github.com/Lissy93/dashy/issues/new/choose">open a ticket on GitHub</a>
|
||||||
|
</li>
|
||||||
|
<li>Click 'Ignore Critical Errors' below to not show this warning again</li>
|
||||||
|
</ul>
|
||||||
|
<button class="user-doesnt-care" @click="ignoreWarning">
|
||||||
|
{{ $t('critical-error.ignore-button') }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import { localStorageKeys } from '@/utils/defaults';
|
||||||
|
import Keys from '@/utils/StoreMutations';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'CriticalError',
|
||||||
|
computed: {
|
||||||
|
/* Determines if we should show this component.
|
||||||
|
* If error present AND user hasn't disabled */
|
||||||
|
shouldShow() {
|
||||||
|
return this.$store.state.criticalError
|
||||||
|
&& !localStorage[localStorageKeys.DISABLE_CRITICAL_WARNING];
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
/* Ignore all future errors, by putting a key in local storage */
|
||||||
|
ignoreWarning() {
|
||||||
|
localStorage.setItem(localStorageKeys.DISABLE_CRITICAL_WARNING, true);
|
||||||
|
this.close();
|
||||||
|
},
|
||||||
|
/* Close this dialog, by removing this error from the local store */
|
||||||
|
close() {
|
||||||
|
this.$store.commit(Keys.CRITICAL_ERROR_MSG, null);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style scoped lang="scss">
|
||||||
|
@import '@/styles/media-queries.scss';
|
||||||
|
.critical-error-wrap {
|
||||||
|
position: absolute;
|
||||||
|
top: 40%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
z-index: 3;
|
||||||
|
max-width: 50rem;
|
||||||
|
background: var(--background-darker);
|
||||||
|
padding: 1rem;
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
color: var(--danger);
|
||||||
|
border: 2px solid var(--danger);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
@include tablet-down {
|
||||||
|
top: 50%;
|
||||||
|
width: 85vw;
|
||||||
|
}
|
||||||
|
p, ul, h4, a {
|
||||||
|
margin: 0;
|
||||||
|
color: var(--white);
|
||||||
|
}
|
||||||
|
pre {
|
||||||
|
color: var(--warning);
|
||||||
|
font-size: 0.8rem;
|
||||||
|
overflow: auto;
|
||||||
|
background: var(--transparent-white-10);
|
||||||
|
padding: 0.5rem;
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
}
|
||||||
|
h4 {
|
||||||
|
margin: 0.5rem 0 0 0;
|
||||||
|
font-size: 1.2rem;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
font-size: 2.2rem;
|
||||||
|
text-align: center;
|
||||||
|
background: var(--danger);
|
||||||
|
color: var(--white);
|
||||||
|
margin: -1rem -1rem 1rem -1rem;
|
||||||
|
padding: 0.5rem;
|
||||||
|
}
|
||||||
|
ul {
|
||||||
|
padding-left: 1rem;
|
||||||
|
}
|
||||||
|
.user-doesnt-care {
|
||||||
|
background: var(--background-darker);
|
||||||
|
color: var(--white);
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
border: none;
|
||||||
|
text-decoration: underline;
|
||||||
|
padding: 0.25rem 0.5rem;
|
||||||
|
cursor: pointer;
|
||||||
|
width: fit-content;
|
||||||
|
margin: 0 auto;
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background: var(--danger);
|
||||||
|
color: var(--background-darker);
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.close {
|
||||||
|
position: absolute;
|
||||||
|
top: 1rem;
|
||||||
|
right: 1rem;
|
||||||
|
width: 1.5rem;
|
||||||
|
height: 1.5rem;
|
||||||
|
padding: 0;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 1rem;
|
||||||
|
background: var(--background);
|
||||||
|
color: var(--primary);
|
||||||
|
border: none;
|
||||||
|
border-radius: var(--curve-factor);
|
||||||
|
transition: all 0.2s ease-in-out;
|
||||||
|
&:hover {
|
||||||
|
background: var(--primary);
|
||||||
|
color: var(--background);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
@ -37,8 +37,10 @@ export default {
|
|||||||
input: '',
|
input: '',
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
props: {
|
computed: {
|
||||||
iconSize: String,
|
iconSize() {
|
||||||
|
return this.$store.getters.iconSize;
|
||||||
|
},
|
||||||
},
|
},
|
||||||
components: {
|
components: {
|
||||||
IconSmall,
|
IconSmall,
|
||||||
|
@ -5,19 +5,19 @@
|
|||||||
<IconDeafault
|
<IconDeafault
|
||||||
@click="updateDisplayLayout('auto')"
|
@click="updateDisplayLayout('auto')"
|
||||||
v-tooltip="tooltip($t('settings.layout-auto'))"
|
v-tooltip="tooltip($t('settings.layout-auto'))"
|
||||||
:class="`layout-icon ${displayLayout === 'auto' ? 'selected' : ''}`"
|
:class="`layout-icon ${layout === 'auto' ? 'selected' : ''}`"
|
||||||
tabindex="-2"
|
tabindex="-2"
|
||||||
/>
|
/>
|
||||||
<IconHorizontal
|
<IconHorizontal
|
||||||
@click="updateDisplayLayout('horizontal')"
|
@click="updateDisplayLayout('horizontal')"
|
||||||
v-tooltip="tooltip($t('settings.layout-horizontal'))"
|
v-tooltip="tooltip($t('settings.layout-horizontal'))"
|
||||||
:class="`layout-icon ${displayLayout === 'horizontal' ? 'selected' : ''}`"
|
:class="`layout-icon ${layout === 'horizontal' ? 'selected' : ''}`"
|
||||||
tabindex="-2"
|
tabindex="-2"
|
||||||
/>
|
/>
|
||||||
<IconVertical
|
<IconVertical
|
||||||
@click="updateDisplayLayout('vertical')"
|
@click="updateDisplayLayout('vertical')"
|
||||||
v-tooltip="tooltip($t('settings.layout-vertical'))"
|
v-tooltip="tooltip($t('settings.layout-vertical'))"
|
||||||
:class="`layout-icon ${displayLayout === 'vertical' ? 'selected' : ''}`"
|
:class="`layout-icon ${layout === 'vertical' ? 'selected' : ''}`"
|
||||||
tabindex="-2"
|
tabindex="-2"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@ -40,6 +40,11 @@ export default {
|
|||||||
IconHorizontal,
|
IconHorizontal,
|
||||||
IconVertical,
|
IconVertical,
|
||||||
},
|
},
|
||||||
|
computed: {
|
||||||
|
layout() {
|
||||||
|
return this.$store.getters.layout;
|
||||||
|
},
|
||||||
|
},
|
||||||
methods: {
|
methods: {
|
||||||
updateDisplayLayout(layout) {
|
updateDisplayLayout(layout) {
|
||||||
this.$store.commit(StoreKeys.SET_ITEM_LAYOUT, layout);
|
this.$store.commit(StoreKeys.SET_ITEM_LAYOUT, layout);
|
||||||
|
@ -7,7 +7,7 @@
|
|||||||
<div class="options-outer">
|
<div class="options-outer">
|
||||||
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
|
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
|
||||||
<ThemeSelector />
|
<ThemeSelector />
|
||||||
<LayoutSelector :displayLayout="displayLayout" />
|
<LayoutSelector :displayLayout="$store.getters.layout" />
|
||||||
<ItemSizeSelector :iconSize="iconSize" />
|
<ItemSizeSelector :iconSize="iconSize" />
|
||||||
<ConfigLauncher />
|
<ConfigLauncher />
|
||||||
<AuthButtons v-if="userState !== 0" :userType="userState" />
|
<AuthButtons v-if="userState !== 0" :userType="userState" />
|
||||||
|
@ -1,9 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="readme-stats">
|
<div class="readme-stats">
|
||||||
<img class="stats-card" v-if="!hideProfileCard" :src="profileCard" alt="Profile Card" />
|
<a v-if="!hideProfileCard" :href="profileCardLink" target="_blank">
|
||||||
<img class="stats-card" v-if="!hideLanguagesCard" :src="topLanguagesCard" alt="Languages" />
|
<img class="stats-card" :src="profileCard" alt="Profile Card" />
|
||||||
|
</a>
|
||||||
|
<a v-if="!hideLanguagesCard" :href="profileCardLink" target="_blank">
|
||||||
|
<img class="stats-card" :src="topLanguagesCard" alt="Languages" />
|
||||||
|
</a>
|
||||||
<template v-if="repos">
|
<template v-if="repos">
|
||||||
<img class="stats-card" v-for="(repo, i) in repoCards" :key="i" :src="repo" :alt="repo" />
|
<a v-for="(repo, i) in repoCards" :key="i" :href="repo.cardHref" target="_blank">
|
||||||
|
<img class="stats-card" :src="repo.cardSrc" :alt="repo" />
|
||||||
|
</a>
|
||||||
</template>
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@ -61,6 +67,9 @@ export default {
|
|||||||
profileCard() {
|
profileCard() {
|
||||||
return `${widgetApiEndpoints.readMeStats}?username=${this.username}${this.cardConfig}`;
|
return `${widgetApiEndpoints.readMeStats}?username=${this.username}${this.cardConfig}`;
|
||||||
},
|
},
|
||||||
|
profileCardLink() {
|
||||||
|
return `https://github.com/${this.username}`;
|
||||||
|
},
|
||||||
topLanguagesCard() {
|
topLanguagesCard() {
|
||||||
return `${widgetApiEndpoints.readMeStats}/top-langs/?username=${this.username}`
|
return `${widgetApiEndpoints.readMeStats}/top-langs/?username=${this.username}`
|
||||||
+ `${this.cardConfig}&langs_count=12`;
|
+ `${this.cardConfig}&langs_count=12`;
|
||||||
@ -70,8 +79,11 @@ export default {
|
|||||||
this.repos.forEach((repo) => {
|
this.repos.forEach((repo) => {
|
||||||
const username = repo.split('/')[0];
|
const username = repo.split('/')[0];
|
||||||
const repoName = repo.split('/')[1];
|
const repoName = repo.split('/')[1];
|
||||||
cards.push(`${widgetApiEndpoints.readMeStats}/pin/?username=${username}&repo=${repoName}`
|
cards.push({
|
||||||
+ `${this.cardConfig}&show_owner=true`);
|
cardSrc: `${widgetApiEndpoints.readMeStats}/pin/?username=${username}`
|
||||||
|
+ `&repo=${repoName}${this.cardConfig}&show_owner=true`,
|
||||||
|
cardHref: `https://github.com/${username}/${repoName}`,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
return cards;
|
return cards;
|
||||||
},
|
},
|
||||||
|
16
src/main.js
16
src/main.js
@ -23,6 +23,7 @@ import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/u
|
|||||||
import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth';
|
import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth';
|
||||||
import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth';
|
import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth';
|
||||||
import Keys from '@/utils/StoreMutations';
|
import Keys from '@/utils/StoreMutations';
|
||||||
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
|
|
||||||
// Initialize global Vue components
|
// Initialize global Vue components
|
||||||
Vue.use(VueI18n);
|
Vue.use(VueI18n);
|
||||||
@ -61,16 +62,19 @@ const mount = () => new Vue({
|
|||||||
}).$mount('#app');
|
}).$mount('#app');
|
||||||
|
|
||||||
store.dispatch(Keys.INITIALIZE_CONFIG).then(() => {
|
store.dispatch(Keys.INITIALIZE_CONFIG).then(() => {
|
||||||
// Keycloak is enabled, redirect to KC login page
|
if (isKeycloakEnabled()) { // If Keycloak is enabled, initialize auth
|
||||||
if (isKeycloakEnabled()) {
|
|
||||||
initKeycloakAuth()
|
initKeycloakAuth()
|
||||||
.then(() => mount())
|
.then(() => mount())
|
||||||
.catch(() => window.location.reload());
|
.catch((e) => {
|
||||||
} else if (isHeaderAuthEnabled()) {
|
ErrorHandler('Failed to authenticate with Keycloak', e);
|
||||||
|
});
|
||||||
|
} else if (isHeaderAuthEnabled()) { // If header auth is enabled, initialize auth
|
||||||
initHeaderAuth()
|
initHeaderAuth()
|
||||||
.then(() => mount())
|
.then(() => mount())
|
||||||
.catch(() => window.location.reload());
|
.catch((e) => {
|
||||||
} else { // If Keycloak not enabled, then proceed straight to the app
|
ErrorHandler('Failed to authenticate with server', e);
|
||||||
|
});
|
||||||
|
} else { // If no third-party auth, just mount the app as normal
|
||||||
mount();
|
mount();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
@ -28,19 +28,24 @@ const HomeMixin = {
|
|||||||
return this.$store.state.modalOpen;
|
return this.$store.state.modalOpen;
|
||||||
},
|
},
|
||||||
pageId() {
|
pageId() {
|
||||||
return (this.subPageInfo && this.subPageInfo.pageId) ? this.subPageInfo.pageId : 'home';
|
return this.$store.state.currentConfigInfo?.confId || 'home';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
data: () => ({
|
data: () => ({
|
||||||
searchValue: '',
|
searchValue: '',
|
||||||
}),
|
}),
|
||||||
async mounted() {
|
|
||||||
// await this.getConfigForRoute();
|
|
||||||
},
|
|
||||||
watch: {
|
watch: {
|
||||||
async $route() {
|
async $route() {
|
||||||
this.loadUpConfig();
|
this.loadUpConfig();
|
||||||
},
|
},
|
||||||
|
pageInfo: {
|
||||||
|
handler(newPageInfo) {
|
||||||
|
if (newPageInfo && newPageInfo.title) {
|
||||||
|
document.title = newPageInfo.title;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
immediate: true,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
async created() {
|
async created() {
|
||||||
this.loadUpConfig();
|
this.loadUpConfig();
|
||||||
@ -79,6 +84,14 @@ const HomeMixin = {
|
|||||||
searching(searchValue) {
|
searching(searchValue) {
|
||||||
this.searchValue = searchValue || '';
|
this.searchValue = searchValue || '';
|
||||||
},
|
},
|
||||||
|
/* Returns a unique ID based on the page and section name */
|
||||||
|
makeSectionId(section) {
|
||||||
|
const normalize = (str) => (
|
||||||
|
str ? str.trim().toLowerCase().replace(/[^a-zA-Z0-9]/g, '-')
|
||||||
|
: `unnamed-${(`000${Math.floor(Math.random() * 1000)}`).slice(-3)}`
|
||||||
|
);
|
||||||
|
return `${this.pageId || 'unknown-page'}-${normalize(section.name)}`;
|
||||||
|
},
|
||||||
/* Returns true if there is one or more sections in the config */
|
/* Returns true if there is one or more sections in the config */
|
||||||
checkTheresData(sections) {
|
checkTheresData(sections) {
|
||||||
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
|
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];
|
||||||
|
@ -105,10 +105,18 @@ const WidgetMixin = {
|
|||||||
const method = protocol || 'GET';
|
const method = protocol || 'GET';
|
||||||
const url = this.useProxy ? this.proxyReqEndpoint : endpoint;
|
const url = this.useProxy ? this.proxyReqEndpoint : endpoint;
|
||||||
const data = JSON.stringify(body || {});
|
const data = JSON.stringify(body || {});
|
||||||
|
|
||||||
const CustomHeaders = options || {};
|
const CustomHeaders = options || {};
|
||||||
const headers = new Headers(this.useProxy
|
const headers = new Headers();
|
||||||
? ({ ...CustomHeaders, 'Target-URL': endpoint })
|
|
||||||
: CustomHeaders);
|
// If using a proxy, set the 'Target-URL' header
|
||||||
|
if (this.useProxy) {
|
||||||
|
headers.append('Target-URL', endpoint);
|
||||||
|
}
|
||||||
|
// Apply widget-specific custom headers
|
||||||
|
Object.entries(CustomHeaders).forEach(([key, value]) => {
|
||||||
|
headers.append(key, value);
|
||||||
|
});
|
||||||
|
|
||||||
// If the request is a GET, delete the body
|
// If the request is a GET, delete the body
|
||||||
const bodyContent = method.toUpperCase() === 'GET' ? undefined : data;
|
const bodyContent = method.toUpperCase() === 'GET' ? undefined : data;
|
||||||
|
102
src/store.js
102
src/store.js
@ -8,7 +8,7 @@ import { makePageName, formatConfigPath, componentVisibility } from '@/utils/Con
|
|||||||
import { applyItemId } from '@/utils/SectionHelpers';
|
import { applyItemId } from '@/utils/SectionHelpers';
|
||||||
import filterUserSections from '@/utils/CheckSectionVisibility';
|
import filterUserSections from '@/utils/CheckSectionVisibility';
|
||||||
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
import ErrorHandler, { InfoHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||||
import { isUserAdmin } from '@/utils/Auth';
|
import { isUserAdmin, makeBasicAuthHeaders, isLoggedInAsGuest } from '@/utils/Auth';
|
||||||
import { localStorageKeys, theme as defaultTheme } from './utils/defaults';
|
import { localStorageKeys, theme as defaultTheme } from './utils/defaults';
|
||||||
|
|
||||||
Vue.use(Vuex);
|
Vue.use(Vuex);
|
||||||
@ -41,8 +41,15 @@ const {
|
|||||||
INSERT_ITEM,
|
INSERT_ITEM,
|
||||||
UPDATE_CUSTOM_CSS,
|
UPDATE_CUSTOM_CSS,
|
||||||
CONF_MENU_INDEX,
|
CONF_MENU_INDEX,
|
||||||
|
CRITICAL_ERROR_MSG,
|
||||||
} = Keys;
|
} = Keys;
|
||||||
|
|
||||||
|
const emptyConfig = {
|
||||||
|
appConfig: {},
|
||||||
|
pageInfo: { title: 'Dashy' },
|
||||||
|
sections: [],
|
||||||
|
};
|
||||||
|
|
||||||
const store = new Vuex.Store({
|
const store = new Vuex.Store({
|
||||||
state: {
|
state: {
|
||||||
config: {}, // The current config being used, and rendered to the UI
|
config: {}, // The current config being used, and rendered to the UI
|
||||||
@ -51,6 +58,7 @@ const store = new Vuex.Store({
|
|||||||
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
|
modalOpen: false, // KB shortcut functionality will be disabled when modal is open
|
||||||
currentConfigInfo: {}, // For multi-page support, will store info about config file
|
currentConfigInfo: {}, // For multi-page support, will store info about config file
|
||||||
isUsingLocalConfig: false, // If true, will use local config instead of fetched
|
isUsingLocalConfig: false, // If true, will use local config instead of fetched
|
||||||
|
criticalError: null, // Will store a message, if a critical error occurs
|
||||||
navigateConfToTab: undefined, // Used to switch active tab in config modal
|
navigateConfToTab: undefined, // Used to switch active tab in config modal
|
||||||
},
|
},
|
||||||
getters: {
|
getters: {
|
||||||
@ -106,7 +114,8 @@ const store = new Vuex.Store({
|
|||||||
}
|
}
|
||||||
// Disable everything
|
// Disable everything
|
||||||
if (appConfig.disableConfiguration
|
if (appConfig.disableConfiguration
|
||||||
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())) {
|
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())
|
||||||
|
|| isLoggedInAsGuest()) {
|
||||||
perms.allowWriteToDisk = false;
|
perms.allowWriteToDisk = false;
|
||||||
perms.allowSaveLocally = false;
|
perms.allowSaveLocally = false;
|
||||||
perms.allowViewConfig = false;
|
perms.allowViewConfig = false;
|
||||||
@ -137,10 +146,18 @@ const store = new Vuex.Store({
|
|||||||
return foundSection;
|
return foundSection;
|
||||||
},
|
},
|
||||||
layout(state) {
|
layout(state) {
|
||||||
return state.config.appConfig.layout || 'auto';
|
const pageId = state.currentConfigInfo.confId;
|
||||||
|
const layoutStoreKey = pageId
|
||||||
|
? `${localStorageKeys.LAYOUT_ORIENTATION}-${pageId}` : localStorageKeys.LAYOUT_ORIENTATION;
|
||||||
|
const appConfigLayout = state.config.appConfig.layout;
|
||||||
|
return localStorage.getItem(layoutStoreKey) || appConfigLayout || 'auto';
|
||||||
},
|
},
|
||||||
iconSize(state) {
|
iconSize(state) {
|
||||||
return state.config.appConfig.iconSize || 'medium';
|
const pageId = state.currentConfigInfo.confId;
|
||||||
|
const sizeStoreKey = pageId
|
||||||
|
? `${localStorageKeys.ICON_SIZE}-${pageId}` : localStorageKeys.ICON_SIZE;
|
||||||
|
const appConfigSize = state.config.appConfig.iconSize;
|
||||||
|
return localStorage.getItem(sizeStoreKey) || appConfigSize || 'medium';
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mutations: {
|
mutations: {
|
||||||
@ -174,6 +191,10 @@ const store = new Vuex.Store({
|
|||||||
state.editMode = editMode;
|
state.editMode = editMode;
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
[CRITICAL_ERROR_MSG](state, message) {
|
||||||
|
if (message) ErrorHandler(message);
|
||||||
|
state.criticalError = message;
|
||||||
|
},
|
||||||
[UPDATE_ITEM](state, payload) {
|
[UPDATE_ITEM](state, payload) {
|
||||||
const { itemId, newItem } = payload;
|
const { itemId, newItem } = payload;
|
||||||
const newConfig = { ...state.config };
|
const newConfig = { ...state.config };
|
||||||
@ -298,11 +319,23 @@ const store = new Vuex.Store({
|
|||||||
InfoHandler('Color palette updated', InfoKeys.VISUAL);
|
InfoHandler('Color palette updated', InfoKeys.VISUAL);
|
||||||
},
|
},
|
||||||
[SET_ITEM_LAYOUT](state, layout) {
|
[SET_ITEM_LAYOUT](state, layout) {
|
||||||
state.config.appConfig.layout = layout;
|
const newConfig = { ...state.config };
|
||||||
|
newConfig.appConfig.layout = layout;
|
||||||
|
state.config = newConfig;
|
||||||
|
const pageId = state.currentConfigInfo.confId;
|
||||||
|
const layoutStoreKey = pageId
|
||||||
|
? `${localStorageKeys.LAYOUT_ORIENTATION}-${pageId}` : localStorageKeys.LAYOUT_ORIENTATION;
|
||||||
|
localStorage.setItem(layoutStoreKey, layout);
|
||||||
InfoHandler('Layout updated', InfoKeys.VISUAL);
|
InfoHandler('Layout updated', InfoKeys.VISUAL);
|
||||||
},
|
},
|
||||||
[SET_ITEM_SIZE](state, iconSize) {
|
[SET_ITEM_SIZE](state, iconSize) {
|
||||||
state.config.appConfig.iconSize = iconSize;
|
const newConfig = { ...state.config };
|
||||||
|
newConfig.appConfig.iconSize = iconSize;
|
||||||
|
state.config = newConfig;
|
||||||
|
const pageId = state.currentConfigInfo.confId;
|
||||||
|
const sizeStoreKey = pageId
|
||||||
|
? `${localStorageKeys.ICON_SIZE}-${pageId}` : localStorageKeys.ICON_SIZE;
|
||||||
|
localStorage.setItem(sizeStoreKey, iconSize);
|
||||||
InfoHandler('Item size updated', InfoKeys.VISUAL);
|
InfoHandler('Item size updated', InfoKeys.VISUAL);
|
||||||
},
|
},
|
||||||
[UPDATE_CUSTOM_CSS](state, customCss) {
|
[UPDATE_CUSTOM_CSS](state, customCss) {
|
||||||
@ -320,16 +353,39 @@ const store = new Vuex.Store({
|
|||||||
actions: {
|
actions: {
|
||||||
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
|
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
|
||||||
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
|
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
|
||||||
// Load and parse config from root config file
|
|
||||||
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
|
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
|
||||||
const data = await yaml.load((await axios.get(configFilePath)).data);
|
try {
|
||||||
// Replace missing root properties with empty objects
|
// Attempt to fetch the YAML file
|
||||||
if (!data.appConfig) data.appConfig = {};
|
const response = await axios.get(configFilePath, makeBasicAuthHeaders());
|
||||||
if (!data.pageInfo) data.pageInfo = {};
|
let data;
|
||||||
if (!data.sections) data.sections = [];
|
try {
|
||||||
// Set the state, and return data
|
data = yaml.load(response.data);
|
||||||
commit(SET_ROOT_CONFIG, data);
|
} catch (parseError) {
|
||||||
return data;
|
commit(CRITICAL_ERROR_MSG, `Failed to parse YAML: ${parseError.message}`);
|
||||||
|
return { ...emptyConfig };
|
||||||
|
}
|
||||||
|
// Replace missing root properties with empty objects
|
||||||
|
if (!data.appConfig) data.appConfig = {};
|
||||||
|
if (!data.pageInfo) data.pageInfo = {};
|
||||||
|
if (!data.sections) data.sections = [];
|
||||||
|
// Set the state, and return data
|
||||||
|
commit(SET_ROOT_CONFIG, data);
|
||||||
|
commit(CRITICAL_ERROR_MSG, null);
|
||||||
|
return data;
|
||||||
|
} catch (fetchError) {
|
||||||
|
if (fetchError.response) {
|
||||||
|
commit(
|
||||||
|
CRITICAL_ERROR_MSG,
|
||||||
|
'Failed to fetch configuration: Server responded with status '
|
||||||
|
+ `${fetchError.response?.status || 'mystery status'}`,
|
||||||
|
);
|
||||||
|
} else if (fetchError.request) {
|
||||||
|
commit(CRITICAL_ERROR_MSG, 'Failed to fetch configuration: No response from server');
|
||||||
|
} else {
|
||||||
|
commit(CRITICAL_ERROR_MSG, `Failed to fetch configuration: ${fetchError.message}`);
|
||||||
|
}
|
||||||
|
return { ...emptyConfig };
|
||||||
|
}
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Fetches config and updates state
|
* Fetches config and updates state
|
||||||
@ -339,6 +395,7 @@ const store = new Vuex.Store({
|
|||||||
*/
|
*/
|
||||||
async [INITIALIZE_CONFIG]({ commit, state }, subConfigId) {
|
async [INITIALIZE_CONFIG]({ commit, state }, subConfigId) {
|
||||||
const rootConfig = state.rootConfig || await this.dispatch(Keys.INITIALIZE_ROOT_CONFIG);
|
const rootConfig = state.rootConfig || await this.dispatch(Keys.INITIALIZE_ROOT_CONFIG);
|
||||||
|
|
||||||
commit(SET_IS_USING_LOCAL_CONFIG, false);
|
commit(SET_IS_USING_LOCAL_CONFIG, false);
|
||||||
if (!subConfigId) { // Use root config as config
|
if (!subConfigId) { // Use root config as config
|
||||||
commit(SET_CONFIG, rootConfig);
|
commit(SET_CONFIG, rootConfig);
|
||||||
@ -351,7 +408,7 @@ const store = new Vuex.Store({
|
|||||||
const json = JSON.parse(localSectionsRaw);
|
const json = JSON.parse(localSectionsRaw);
|
||||||
if (json.length >= 1) localSections = json;
|
if (json.length >= 1) localSections = json;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorHandler('Malformed section data in local storage');
|
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (localSections.length > 0) {
|
if (localSections.length > 0) {
|
||||||
@ -366,11 +423,10 @@ const store = new Vuex.Store({
|
|||||||
)?.path);
|
)?.path);
|
||||||
|
|
||||||
if (!subConfigPath) {
|
if (!subConfigPath) {
|
||||||
ErrorHandler(`Unable to find config for '${subConfigId}'`);
|
commit(CRITICAL_ERROR_MSG, `Unable to find config for '${subConfigId}'`);
|
||||||
return null;
|
return { ...emptyConfig };
|
||||||
}
|
}
|
||||||
|
axios.get(subConfigPath, makeBasicAuthHeaders()).then((response) => {
|
||||||
axios.get(subConfigPath).then((response) => {
|
|
||||||
// Parse the YAML
|
// Parse the YAML
|
||||||
const configContent = yaml.load(response.data) || {};
|
const configContent = yaml.load(response.data) || {};
|
||||||
// Certain values must be inherited from root config
|
// Certain values must be inherited from root config
|
||||||
@ -389,17 +445,17 @@ const store = new Vuex.Store({
|
|||||||
commit(SET_IS_USING_LOCAL_CONFIG, true);
|
commit(SET_IS_USING_LOCAL_CONFIG, true);
|
||||||
}
|
}
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ErrorHandler('Malformed section data in local storage for sub-config');
|
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage for sub-config');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Set the config
|
// Set the config
|
||||||
commit(SET_CONFIG, configContent);
|
commit(SET_CONFIG, configContent);
|
||||||
commit(SET_CURRENT_CONFIG_INFO, { confPath: subConfigPath, confId: subConfigId });
|
commit(SET_CURRENT_CONFIG_INFO, { confPath: subConfigPath, confId: subConfigId });
|
||||||
}).catch((err) => {
|
}).catch((err) => {
|
||||||
ErrorHandler(`Unable to load config from '${subConfigPath}'`, err);
|
commit(CRITICAL_ERROR_MSG, `Unable to load config from '${subConfigPath}'`, err);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return null;
|
return { ...emptyConfig };
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
modules: {},
|
modules: {},
|
||||||
|
@ -31,6 +31,7 @@
|
|||||||
--transparent-white-70: #ffffffb3;
|
--transparent-white-70: #ffffffb3;
|
||||||
--transparent-white-50: #ffffff80;
|
--transparent-white-50: #ffffff80;
|
||||||
--transparent-white-30: #ffffff4d;
|
--transparent-white-30: #ffffff4d;
|
||||||
|
--transparent-white-10: #ffffff0f;
|
||||||
|
|
||||||
/* Color variables for specific components
|
/* Color variables for specific components
|
||||||
* all variables are optional, since they inherit initial values from above*
|
* all variables are optional, since they inherit initial values from above*
|
||||||
|
@ -1717,6 +1717,7 @@ html[data-theme='neomorphic'] {
|
|||||||
.config-buttons > svg,
|
.config-buttons > svg,
|
||||||
.display-options svg,
|
.display-options svg,
|
||||||
form.minimal input,
|
form.minimal input,
|
||||||
|
.critical-error-wrap button.user-doesnt-care,
|
||||||
a.config-button, button.config-button {
|
a.config-button, button.config-button {
|
||||||
border-radius: 0.35rem;
|
border-radius: 0.35rem;
|
||||||
box-shadow: var(--glass-button-shadow);
|
box-shadow: var(--glass-button-shadow);
|
||||||
@ -1724,6 +1725,7 @@ html[data-theme='neomorphic'] {
|
|||||||
border: 1px solid rgba(255, 255, 255, 0.19);
|
border: 1px solid rgba(255, 255, 255, 0.19);
|
||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
transition: all 0.2s ease-in-out;
|
transition: all 0.2s ease-in-out;
|
||||||
|
text-decoration: none;
|
||||||
&:hover, &.selected {
|
&:hover, &.selected {
|
||||||
box-shadow: var(--glass-button-hover-shadow);
|
box-shadow: var(--glass-button-hover-shadow);
|
||||||
border: 1px solid rgba(255, 255, 255, 0.25) !important;
|
border: 1px solid rgba(255, 255, 255, 0.25) !important;
|
||||||
@ -1791,6 +1793,11 @@ html[data-theme='neomorphic'] {
|
|||||||
background: rgba(255, 255, 255, 0.15);
|
background: rgba(255, 255, 255, 0.15);
|
||||||
backdrop-filter: blur(50px);
|
backdrop-filter: blur(50px);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.critical-error-wrap {
|
||||||
|
backdrop-filter: blur(15px);
|
||||||
|
background: #0f0528c4;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
html[data-theme='glass'] {
|
html[data-theme='glass'] {
|
||||||
|
@ -22,6 +22,11 @@ html {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#dashy {
|
||||||
|
position: relative;
|
||||||
|
min-height: 100vh;
|
||||||
|
}
|
||||||
|
|
||||||
/* Hide text, and show 'Loading...' while Vue is initializing tags */
|
/* Hide text, and show 'Loading...' while Vue is initializing tags */
|
||||||
[v-cloak] > * { display:none }
|
[v-cloak] > * { display:none }
|
||||||
[v-cloak]::before { content: "loading…" }
|
[v-cloak]::before { content: "loading…" }
|
||||||
|
@ -11,8 +11,6 @@ const getAppConfig = () => {
|
|||||||
return config.appConfig || {};
|
return config.appConfig || {};
|
||||||
};
|
};
|
||||||
|
|
||||||
// const appConfig = $store.getters.appConfig || {};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the user is still using array for users, prints warning
|
* Called when the user is still using array for users, prints warning
|
||||||
* This was a breaking change, implemented in V 1.6.5
|
* This was a breaking change, implemented in V 1.6.5
|
||||||
@ -41,37 +39,52 @@ const getUsers = () => {
|
|||||||
* @returns {String} The hashed token
|
* @returns {String} The hashed token
|
||||||
*/
|
*/
|
||||||
const generateUserToken = (user) => {
|
const generateUserToken = (user) => {
|
||||||
if (!user.user || !user.hash) {
|
if (!user.user || (!user.hash && !user.password)) {
|
||||||
ErrorHandler('Invalid user object. Must have `user` and `hash` parameters');
|
ErrorHandler('Invalid user object. Must have `user` and either a `hash` or `password` param');
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
const passHash = user.hash || sha256(process.env[user.password]).toString().toUpperCase();
|
||||||
const strAndUpper = (input) => input.toString().toUpperCase();
|
const strAndUpper = (input) => input.toString().toUpperCase();
|
||||||
const sha = sha256(strAndUpper(user.user) + strAndUpper(user.hash));
|
const sha = sha256(strAndUpper(user.user) + strAndUpper(passHash));
|
||||||
return strAndUpper(sha);
|
return strAndUpper(sha);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCookieToken = () => {
|
||||||
|
const value = `; ${document.cookie}`;
|
||||||
|
const parts = value.split(`; ${cookieKeys.AUTH_TOKEN}=`);
|
||||||
|
if (parts.length === 2) return parts.pop().split(';').shift();
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const makeBasicAuthHeaders = () => {
|
||||||
|
const token = getCookieToken();
|
||||||
|
const bearerAuth = (token && token.length > 5) ? `Bearer ${token}` : null;
|
||||||
|
|
||||||
|
const username = process.env.VUE_APP_BASIC_AUTH_USERNAME
|
||||||
|
|| localStorage[localStorageKeys.USERNAME]
|
||||||
|
|| 'user';
|
||||||
|
const password = process.env.VUE_APP_BASIC_AUTH_PASSWORD || bearerAuth;
|
||||||
|
const basicAuth = `Basic ${btoa(`${username}:${password}`)}`;
|
||||||
|
|
||||||
|
const headers = password
|
||||||
|
? { headers: { Authorization: basicAuth, 'WWW-Authenticate': 'true' } }
|
||||||
|
: {};
|
||||||
|
return headers;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Checks if the user is currently authenticated
|
* Checks if the user is currently authenticated
|
||||||
* @returns {Boolean} Will return true if the user is logged in, else false
|
* @returns {Boolean} Will return true if the user is logged in, else false
|
||||||
*/
|
*/
|
||||||
export const isLoggedIn = () => {
|
export const isLoggedIn = () => {
|
||||||
const users = getUsers();
|
const users = getUsers();
|
||||||
let userAuthenticated = document.cookie.split(';').some((cookie) => {
|
const cookieToken = getCookieToken();
|
||||||
if (cookie && cookie.split('=').length > 1) {
|
return users.some((user) => {
|
||||||
const cookieKey = cookie.split('=')[0].trim();
|
if (generateUserToken(user) === cookieToken) {
|
||||||
const cookieValue = cookie.split('=')[1].trim();
|
localStorage.setItem(localStorageKeys.USERNAME, user.user);
|
||||||
if (cookieKey === cookieKeys.AUTH_TOKEN) {
|
return true;
|
||||||
userAuthenticated = users.some((user) => {
|
|
||||||
if (generateUserToken(user) === cookieValue) {
|
|
||||||
localStorage.setItem(localStorageKeys.USERNAME, user.user);
|
|
||||||
return true;
|
|
||||||
} else return false;
|
|
||||||
});
|
|
||||||
return userAuthenticated;
|
|
||||||
} else return false;
|
|
||||||
} else return false;
|
} else return false;
|
||||||
});
|
});
|
||||||
return userAuthenticated;
|
|
||||||
};
|
};
|
||||||
|
|
||||||
/* Returns true if authentication is enabled */
|
/* Returns true if authentication is enabled */
|
||||||
@ -108,7 +121,18 @@ export const checkCredentials = (username, pass, users, messages) => {
|
|||||||
} else {
|
} else {
|
||||||
users.forEach((user) => {
|
users.forEach((user) => {
|
||||||
if (user.user.toLowerCase() === username.toLowerCase()) { // User found
|
if (user.user.toLowerCase() === username.toLowerCase()) { // User found
|
||||||
if (user.hash.toLowerCase() === sha256(pass).toString().toLowerCase()) {
|
if (user.password) {
|
||||||
|
if (!user.password.startsWith('VUE_APP_')) {
|
||||||
|
ErrorHandler('Invalid password format. Please use VUE_APP_ prefix');
|
||||||
|
response = { correct: false, msg: messages.incorrectPassword };
|
||||||
|
} else if (!process.env[user.password]) {
|
||||||
|
ErrorHandler(`Missing environmental variable for ${user.password}`);
|
||||||
|
} else if (process.env[user.password] === pass) {
|
||||||
|
response = { correct: true, msg: messages.successMsg };
|
||||||
|
} else {
|
||||||
|
response = { correct: false, msg: messages.incorrectPassword };
|
||||||
|
}
|
||||||
|
} else if (user.hash && user.hash.toLowerCase() === sha256(pass).toString().toLowerCase()) {
|
||||||
response = { correct: true, msg: messages.successMsg }; // Password is correct
|
response = { correct: true, msg: messages.successMsg }; // Password is correct
|
||||||
} else { // User found, but password is not a match
|
} else { // User found, but password is not a match
|
||||||
response = { correct: false, msg: messages.incorrectPassword };
|
response = { correct: false, msg: messages.incorrectPassword };
|
||||||
@ -163,9 +187,9 @@ export const getCurrentUser = () => {
|
|||||||
* Checks if the user is viewing the dashboard as a guest
|
* Checks if the user is viewing the dashboard as a guest
|
||||||
* Returns true if guest mode enabled, and user not logged in
|
* Returns true if guest mode enabled, and user not logged in
|
||||||
* */
|
* */
|
||||||
export const isLoggedInAsGuest = (currentUser) => {
|
export const isLoggedInAsGuest = () => {
|
||||||
const guestEnabled = isGuestAccessEnabled();
|
const guestEnabled = isGuestAccessEnabled();
|
||||||
const loggedIn = isLoggedIn() && currentUser;
|
const loggedIn = isLoggedIn();
|
||||||
return guestEnabled && !loggedIn;
|
return guestEnabled && !loggedIn;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
@ -500,14 +500,9 @@
|
|||||||
"users": {
|
"users": {
|
||||||
"title": "Users",
|
"title": "Users",
|
||||||
"type": "array",
|
"type": "array",
|
||||||
"description": "Usernames and hashed credentials for frontend authentication",
|
"description": "Usernames and hashed credentials for frontend authentication. Needs to be set at build-time.",
|
||||||
"items": {
|
"items": {
|
||||||
"type": "object",
|
"type": "object",
|
||||||
"additionalProperties": false,
|
|
||||||
"required": [
|
|
||||||
"user",
|
|
||||||
"hash"
|
|
||||||
],
|
|
||||||
"properties": {
|
"properties": {
|
||||||
"user": {
|
"user": {
|
||||||
"title": "Username",
|
"title": "Username",
|
||||||
@ -521,6 +516,12 @@
|
|||||||
"minLength": 64,
|
"minLength": 64,
|
||||||
"maxLength": 64
|
"maxLength": 64
|
||||||
},
|
},
|
||||||
|
"password": {
|
||||||
|
"title": "Password",
|
||||||
|
"type": "string",
|
||||||
|
"description": "The environmental variable pointing to a plaintext password for that user. Must start with VUE_APP_",
|
||||||
|
"pattern": "^VUE_APP_.*"
|
||||||
|
},
|
||||||
"type": {
|
"type": {
|
||||||
"title": "Privileges",
|
"title": "Privileges",
|
||||||
"type": "string",
|
"type": "string",
|
||||||
@ -531,9 +532,15 @@
|
|||||||
"description": "User type, denoting privilege level, either admin or normal",
|
"description": "User type, denoting privilege level, either admin or normal",
|
||||||
"default": "normal"
|
"default": "normal"
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
|
"additionalProperties": false,
|
||||||
|
"required": ["user"],
|
||||||
|
"oneOf": [
|
||||||
|
{ "required": ["hash"] },
|
||||||
|
{ "required": ["password"] }
|
||||||
|
]
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"enableHeaderAuth": {
|
"enableHeaderAuth": {
|
||||||
"title": "Enable HeaderAuth?",
|
"title": "Enable HeaderAuth?",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
|
@ -3,7 +3,7 @@ import sha256 from 'crypto-js/sha256';
|
|||||||
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
import ConfigAccumulator from '@/utils/ConfigAccumalator';
|
||||||
import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
|
import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
|
||||||
import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler';
|
import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler';
|
||||||
import { logout } from '@/utils/Auth';
|
import { logout as authLogout } from '@/utils/Auth';
|
||||||
|
|
||||||
const getAppConfig = () => {
|
const getAppConfig = () => {
|
||||||
const Accumulator = new ConfigAccumulator();
|
const Accumulator = new ConfigAccumulator();
|
||||||
@ -22,7 +22,6 @@ class HeaderAuth {
|
|||||||
this.users = auth.users;
|
this.users = auth.users;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* eslint-disable class-methods-use-this */
|
|
||||||
login() {
|
login() {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
|
||||||
@ -44,6 +43,7 @@ class HeaderAuth {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
ErrorHandler('Error while trying to login using header authentication', e);
|
||||||
reject(e);
|
reject(e);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -51,8 +51,9 @@ class HeaderAuth {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// eslint-disable-next-line class-methods-use-this
|
||||||
logout() {
|
logout() {
|
||||||
logout();
|
authLogout();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -27,7 +27,7 @@ const determineIntersection = (source = [], target = []) => {
|
|||||||
/* Returns false if the displayData of a section/item
|
/* Returns false if the displayData of a section/item
|
||||||
should not be rendered for the current user/ guest */
|
should not be rendered for the current user/ guest */
|
||||||
export const isVisibleToUser = (displayData, currentUser) => {
|
export const isVisibleToUser = (displayData, currentUser) => {
|
||||||
const isGuest = isLoggedInAsGuest(currentUser); // Check if current user is a guest
|
const isGuest = isLoggedInAsGuest(); // Check if current user is a guest
|
||||||
|
|
||||||
// Checks if user explicitly has access to a certain section
|
// Checks if user explicitly has access to a certain section
|
||||||
const checkVisibility = () => {
|
const checkVisibility = () => {
|
||||||
|
@ -29,6 +29,7 @@ const KEY_NAMES = [
|
|||||||
'INSERT_ITEM',
|
'INSERT_ITEM',
|
||||||
'UPDATE_CUSTOM_CSS',
|
'UPDATE_CUSTOM_CSS',
|
||||||
'CONF_MENU_INDEX',
|
'CONF_MENU_INDEX',
|
||||||
|
'CRITICAL_ERROR_MSG',
|
||||||
];
|
];
|
||||||
|
|
||||||
// Convert array of key names into an object, and export
|
// Convert array of key names into an object, and export
|
||||||
|
@ -135,6 +135,7 @@ module.exports = {
|
|||||||
MOST_USED: 'mostUsed',
|
MOST_USED: 'mostUsed',
|
||||||
LAST_USED: 'lastUsed',
|
LAST_USED: 'lastUsed',
|
||||||
KEYCLOAK_INFO: 'keycloakInfo',
|
KEYCLOAK_INFO: 'keycloakInfo',
|
||||||
|
DISABLE_CRITICAL_WARNING: 'disableCriticalWarning',
|
||||||
},
|
},
|
||||||
/* Key names for cookie identifiers */
|
/* Key names for cookie identifiers */
|
||||||
cookieKeys: {
|
cookieKeys: {
|
||||||
|
@ -19,14 +19,7 @@
|
|||||||
</router-link>
|
</router-link>
|
||||||
</div>
|
</div>
|
||||||
<!-- Main content, section for each group of items -->
|
<!-- Main content, section for each group of items -->
|
||||||
<div v-if="checkTheresData(sections) || isEditMode"
|
<div v-if="checkTheresData(sections) || isEditMode" :class="computedClass">
|
||||||
:class="`item-group-container `
|
|
||||||
+ `orientation-${layout} `
|
|
||||||
+ `item-size-${itemSizeBound} `
|
|
||||||
+ (isEditMode ? 'edit-mode ' : '')
|
|
||||||
+ (singleSectionView ? 'single-section-view ' : '')
|
|
||||||
+ (this.colCount ? `col-count-${this.colCount} ` : '')"
|
|
||||||
>
|
|
||||||
<template v-for="(section, index) in filteredSections">
|
<template v-for="(section, index) in filteredSections">
|
||||||
<Section
|
<Section
|
||||||
:key="index"
|
:key="index"
|
||||||
@ -34,7 +27,7 @@
|
|||||||
:title="section.name"
|
:title="section.name"
|
||||||
:icon="section.icon || undefined"
|
:icon="section.icon || undefined"
|
||||||
:displayData="getDisplayData(section)"
|
:displayData="getDisplayData(section)"
|
||||||
:groupId="`${pageId}-section-${index}`"
|
:groupId="makeSectionId(section)"
|
||||||
:items="section.filteredItems"
|
:items="section.filteredItems"
|
||||||
:widgets="section.widgets"
|
:widgets="section.widgets"
|
||||||
:searchTerm="searchValue"
|
:searchTerm="searchValue"
|
||||||
@ -70,7 +63,7 @@ import ExportConfigMenu from '@/components/InteractiveEditor/ExportConfigMenu.vu
|
|||||||
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
|
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
|
||||||
import NotificationThing from '@/components/Settings/LocalConfigWarning.vue';
|
import NotificationThing from '@/components/Settings/LocalConfigWarning.vue';
|
||||||
import StoreKeys from '@/utils/StoreMutations';
|
import StoreKeys from '@/utils/StoreMutations';
|
||||||
import { localStorageKeys, modalNames } from '@/utils/defaults';
|
import { modalNames } from '@/utils/defaults';
|
||||||
import ErrorHandler from '@/utils/ErrorHandler';
|
import ErrorHandler from '@/utils/ErrorHandler';
|
||||||
import BackIcon from '@/assets/interface-icons/back-arrow.svg';
|
import BackIcon from '@/assets/interface-icons/back-arrow.svg';
|
||||||
|
|
||||||
@ -120,19 +113,13 @@ export default {
|
|||||||
iconSize() {
|
iconSize() {
|
||||||
return this.$store.getters.iconSize;
|
return this.$store.getters.iconSize;
|
||||||
},
|
},
|
||||||
},
|
computedClass() {
|
||||||
watch: {
|
let classes = 'item-group-container '
|
||||||
layoutOrientation(layout) {
|
+ ` orientation-${this.$store.getters.layout} item-size-${this.itemSizeBound}`;
|
||||||
if (layout) {
|
if (this.isEditMode) classes += ' edit-mode';
|
||||||
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
|
if (this.singleSectionView) classes += ' single-section-view';
|
||||||
this.layout = layout;
|
if (this.colCount) classes += ` col-count-${this.colCount}`;
|
||||||
}
|
return classes;
|
||||||
},
|
|
||||||
iconSize(size) {
|
|
||||||
if (size) {
|
|
||||||
localStorage.setItem(localStorageKeys.ICON_SIZE, size);
|
|
||||||
this.itemSizeBound = size;
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -34,7 +34,7 @@
|
|||||||
:index="index"
|
:index="index"
|
||||||
:title="section.name"
|
:title="section.name"
|
||||||
:icon="section.icon || undefined"
|
:icon="section.icon || undefined"
|
||||||
:groupId="`section-${index}`"
|
:groupId="makeSectionId(section)"
|
||||||
:items="filterTiles(section.items)"
|
:items="filterTiles(section.items)"
|
||||||
:widgets="section.widgets"
|
:widgets="section.widgets"
|
||||||
:selected="selectedSection === index"
|
:selected="selectedSection === index"
|
||||||
|
264
yarn.lock
264
yarn.lock
@ -1179,93 +1179,93 @@
|
|||||||
mkdirp "^1.0.4"
|
mkdirp "^1.0.4"
|
||||||
rimraf "^3.0.2"
|
rimraf "^3.0.2"
|
||||||
|
|
||||||
"@sentry-internal/feedback@7.110.0":
|
"@sentry-internal/feedback@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.110.0.tgz#7103a08cd6bfb43583087d7476a5f24c5857cc22"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.111.0.tgz#c715e7e6a1877b60cd1f4dff85969660e0deff3f"
|
||||||
integrity sha512-hrfWa3WkSOiBO5Srcr1j4kuGOlbsQic+REpLOofllVIs56DOo9+Aj9svxT+dcvZERv/nlFSV/E0BfGy9g08IEg==
|
integrity sha512-xaKgPPDEirOan7c9HwzYA1KK87kRp/qfIx9ZKLOEtxwy6nqoMuSByGqSwm1Oqfcjpbd7y6/y+7Bw+69ZKNVLDQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "7.110.0"
|
"@sentry/core" "7.111.0"
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sentry-internal/replay-canvas@7.110.0":
|
"@sentry-internal/replay-canvas@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.110.0.tgz#af21b56157f44c44a2eedf4326ef37f4ea440fa8"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.111.0.tgz#aa3cba0477f312cbf40eff4eabeaeda6221a55b6"
|
||||||
integrity sha512-SNa+AfyfX+vc6Xw0pIfDsa5Qnc9cpexU6M2D19gadtVhmep7qoFBuhBVZrSv6BtdCxvrb5EyYsHYGfjQdIDcvg==
|
integrity sha512-3KPBIpiegTYmuVw9gA2aKuliAQONS3Ny1kJc9x5kz6XQGuLFxqlh6KzoCVaKfQJeq2WJqRNeR4KFFuNGuB3H8w==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "7.110.0"
|
"@sentry/core" "7.111.0"
|
||||||
"@sentry/replay" "7.110.0"
|
"@sentry/replay" "7.111.0"
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sentry-internal/tracing@7.110.0":
|
"@sentry-internal/tracing@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.110.0.tgz#00f2086b0efb8dd5a67831074e52b176aa542d32"
|
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.111.0.tgz#b352df9f38009c5d306308a829a1dd9a57f084fd"
|
||||||
integrity sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==
|
integrity sha512-CgXly8rsdu4loWVKi2RqpInH3C2cVBuaYsx4ZP5IJpzSinsUAMyyr3Pc0PZzCyoVpBBXGBGj/4HhFsY3q6Z0Vg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/core" "7.110.0"
|
"@sentry/core" "7.111.0"
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sentry/browser@7.110.0":
|
"@sentry/browser@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.110.0.tgz#40900d76a8f423a7163a594ec9267a2e0ebd8a5b"
|
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.111.0.tgz#29da73e7192eb5643d101c47922d7374e4cc88ed"
|
||||||
integrity sha512-gIxedVm6ZgkjQfgCDgLWJgAsolq6OxV8hQ2j1+RaDL2RngvelFo/vlX5f2sD6EbjVp77Cri8u5GkMJF+v4p84g==
|
integrity sha512-x7S9XoJh+TbMnur4eBhPpCVo+p7udABBV2gQk+Iw6LP9e8EFKmGmNyl76vSsT6GeFJ7mwxDEKfuwbVoLBjIvHw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry-internal/feedback" "7.110.0"
|
"@sentry-internal/feedback" "7.111.0"
|
||||||
"@sentry-internal/replay-canvas" "7.110.0"
|
"@sentry-internal/replay-canvas" "7.111.0"
|
||||||
"@sentry-internal/tracing" "7.110.0"
|
"@sentry-internal/tracing" "7.111.0"
|
||||||
"@sentry/core" "7.110.0"
|
"@sentry/core" "7.111.0"
|
||||||
"@sentry/replay" "7.110.0"
|
"@sentry/replay" "7.111.0"
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sentry/core@7.110.0":
|
"@sentry/core@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.110.0.tgz#2945d3ac0ef116ed313fbfb9da4f483b66fe5bca"
|
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.111.0.tgz#54c9037a3b79b3623377dce1887b69b40670e201"
|
||||||
integrity sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==
|
integrity sha512-/ljeMjZu8CSrLGrseBi/7S2zRIFsqMcvfyG6Nwgfc07J9nbHt8/MqouE1bXZfiaILqDBpK7BK9MLAAph4mkAWg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sentry/replay@7.110.0":
|
"@sentry/replay@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.110.0.tgz#e185c88cec573724b46b79ada7ef5a7098acd1b6"
|
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.111.0.tgz#6d21bddf2ec245db6eb2c471e81efd94364107ae"
|
||||||
integrity sha512-EEpGPf3iBJjWejvoxKLVMnLtLNwPTUxHJV1oxUkbcSi3B/tG5hW7LArYDjAcvkfa4VmA8JLCwj2vYU5MQ8tj6g==
|
integrity sha512-cSbI4A4hrO0sZ0ynvLQauPg8YyaDOQkhGkyvbws8W9WgfxR8X827bY9S0f1TPfgaFiVcKb0iRaAwyXHg3pyzOg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry-internal/tracing" "7.110.0"
|
"@sentry-internal/tracing" "7.111.0"
|
||||||
"@sentry/core" "7.110.0"
|
"@sentry/core" "7.111.0"
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sentry/tracing@^7.102.1":
|
"@sentry/tracing@^7.102.1":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.110.0.tgz#9e8836babba9894309d337f3006b98d79b863329"
|
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.111.0.tgz#3c9da76e6ca1f2e17e138ce828330e93e53f67a4"
|
||||||
integrity sha512-pAydcCqzyzn2Uv9qmuDX5saHbXp4eMMsBW2C/oSkVdKQQSdA7JeG27d82Jz3cMVrfjv105lShP5qS2YjhBTkow==
|
integrity sha512-+BHvdCJxcNnBkru3Y5aFZssEwyNU/mwPTSZqYOhFilokVIrDmVrP/R9g8jHSUqXF4KwB3RaknTPj/4484Z0erA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry-internal/tracing" "7.110.0"
|
"@sentry-internal/tracing" "7.111.0"
|
||||||
|
|
||||||
"@sentry/types@7.110.0":
|
"@sentry/types@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.110.0.tgz#c3f252b008cab905097fc71e174191f20bdaf4f3"
|
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.111.0.tgz#9c869c3c51d606041916765ba58f29de915707ac"
|
||||||
integrity sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==
|
integrity sha512-Oti4pgQ55+FBHKKcHGu51ZUxO1u52G5iVNK4mbtAN+5ArSCy/2s1H8IDJiOMswn3acfUnCR0oB/QsbEgAPZ26g==
|
||||||
|
|
||||||
"@sentry/utils@7.110.0":
|
"@sentry/utils@7.111.0":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.110.0.tgz#68ef59359d608a1a6a7205b780196a042ad73ab2"
|
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.111.0.tgz#e006cc1e751b30ff5cf914c34eb143102e2e8c2d"
|
||||||
integrity sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==
|
integrity sha512-CB5rz1EgCSwj3xoXogsCZ5pQtfERrURc/ItcCuoaijUhkD0iMq5MCNWMHW3mBsBrqx/Oba+XGvDu0t/5+SWwBg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
|
|
||||||
"@sentry/vue@^7.102.1":
|
"@sentry/vue@^7.102.1":
|
||||||
version "7.110.0"
|
version "7.111.0"
|
||||||
resolved "https://registry.yarnpkg.com/@sentry/vue/-/vue-7.110.0.tgz#60bca341bf55c0ebe3bbd5f220241f5e34adbb8b"
|
resolved "https://registry.yarnpkg.com/@sentry/vue/-/vue-7.111.0.tgz#064e1523fc97a81e0a62fc9c40bd905e975fb4f0"
|
||||||
integrity sha512-N8qAAPNJMV9fRMfvbRIWgFrn+wNH6ABGdc7fbFg1y3y0rOw58YMMg0+WdHMGEeWhH7N2/cCJGUHdz4egqaM3gQ==
|
integrity sha512-MEvv+1r7548rMuZF3WbxY2OYxHyjuROMTptYR2xrQj+jEkJ1hFbZyn5J+uH/9OamGY2rksnMqxFBcnfdqrItvA==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@sentry/browser" "7.110.0"
|
"@sentry/browser" "7.111.0"
|
||||||
"@sentry/core" "7.110.0"
|
"@sentry/core" "7.111.0"
|
||||||
"@sentry/types" "7.110.0"
|
"@sentry/types" "7.111.0"
|
||||||
"@sentry/utils" "7.110.0"
|
"@sentry/utils" "7.111.0"
|
||||||
|
|
||||||
"@sideway/address@^4.1.5":
|
"@sideway/address@^4.1.5":
|
||||||
version "4.1.5"
|
version "4.1.5"
|
||||||
@ -1336,9 +1336,9 @@
|
|||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
|
|
||||||
"@types/eslint@*":
|
"@types/eslint@*":
|
||||||
version "8.56.9"
|
version "8.56.10"
|
||||||
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.9.tgz#403e9ced04a34e63f1c383c5b8ee1a94442c8cc4"
|
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d"
|
||||||
integrity sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==
|
integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@types/estree" "*"
|
"@types/estree" "*"
|
||||||
"@types/json-schema" "*"
|
"@types/json-schema" "*"
|
||||||
@ -1436,9 +1436,9 @@
|
|||||||
integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==
|
integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==
|
||||||
|
|
||||||
"@types/qs@*":
|
"@types/qs@*":
|
||||||
version "6.9.14"
|
version "6.9.15"
|
||||||
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b"
|
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce"
|
||||||
integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==
|
integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==
|
||||||
|
|
||||||
"@types/range-parser@*":
|
"@types/range-parser@*":
|
||||||
version "1.2.7"
|
version "1.2.7"
|
||||||
@ -1816,24 +1816,24 @@
|
|||||||
semver "^7.3.4"
|
semver "^7.3.4"
|
||||||
strip-ansi "^6.0.0"
|
strip-ansi "^6.0.0"
|
||||||
|
|
||||||
"@vue/compiler-core@3.4.21":
|
"@vue/compiler-core@3.4.23":
|
||||||
version "3.4.21"
|
version "3.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.21.tgz#868b7085378fc24e58c9aed14c8d62110a62be1a"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.23.tgz#a08f5998e391ad75e602a66dd7255af9054df2f3"
|
||||||
integrity sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==
|
integrity sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/parser" "^7.23.9"
|
"@babel/parser" "^7.24.1"
|
||||||
"@vue/shared" "3.4.21"
|
"@vue/shared" "3.4.23"
|
||||||
entities "^4.5.0"
|
entities "^4.5.0"
|
||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.2.0"
|
||||||
|
|
||||||
"@vue/compiler-dom@3.4.21":
|
"@vue/compiler-dom@3.4.23":
|
||||||
version "3.4.21"
|
version "3.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.23.tgz#6fa622d1e5c8508551564c5dc5948e9cddf60867"
|
||||||
integrity sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==
|
integrity sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-core" "3.4.21"
|
"@vue/compiler-core" "3.4.23"
|
||||||
"@vue/shared" "3.4.21"
|
"@vue/shared" "3.4.23"
|
||||||
|
|
||||||
"@vue/compiler-sfc@2.7.16":
|
"@vue/compiler-sfc@2.7.16":
|
||||||
version "2.7.16"
|
version "2.7.16"
|
||||||
@ -1847,27 +1847,27 @@
|
|||||||
prettier "^1.18.2 || ^2.0.0"
|
prettier "^1.18.2 || ^2.0.0"
|
||||||
|
|
||||||
"@vue/compiler-sfc@^3.4.15":
|
"@vue/compiler-sfc@^3.4.15":
|
||||||
version "3.4.21"
|
version "3.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.23.tgz#7041517b9bbd1b304f0db33bfa424e9a899fda8d"
|
||||||
integrity sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==
|
integrity sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@babel/parser" "^7.23.9"
|
"@babel/parser" "^7.24.1"
|
||||||
"@vue/compiler-core" "3.4.21"
|
"@vue/compiler-core" "3.4.23"
|
||||||
"@vue/compiler-dom" "3.4.21"
|
"@vue/compiler-dom" "3.4.23"
|
||||||
"@vue/compiler-ssr" "3.4.21"
|
"@vue/compiler-ssr" "3.4.23"
|
||||||
"@vue/shared" "3.4.21"
|
"@vue/shared" "3.4.23"
|
||||||
estree-walker "^2.0.2"
|
estree-walker "^2.0.2"
|
||||||
magic-string "^0.30.7"
|
magic-string "^0.30.8"
|
||||||
postcss "^8.4.35"
|
postcss "^8.4.38"
|
||||||
source-map-js "^1.0.2"
|
source-map-js "^1.2.0"
|
||||||
|
|
||||||
"@vue/compiler-ssr@3.4.21":
|
"@vue/compiler-ssr@3.4.23":
|
||||||
version "3.4.21"
|
version "3.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef"
|
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.23.tgz#1ae4afe962a9e156b1a79eff909c37cd423dd4c2"
|
||||||
integrity sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==
|
integrity sha512-hb6Uj2cYs+tfqz71Wj6h3E5t6OKvb4MVcM2Nl5i/z1nv1gjEhw+zYaNOV+Xwn+SSN/VZM0DgANw5TuJfxfezPg==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@vue/compiler-dom" "3.4.21"
|
"@vue/compiler-dom" "3.4.23"
|
||||||
"@vue/shared" "3.4.21"
|
"@vue/shared" "3.4.23"
|
||||||
|
|
||||||
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
|
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
|
||||||
version "3.3.0"
|
version "3.3.0"
|
||||||
@ -1901,10 +1901,10 @@
|
|||||||
resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz#ceb924b4ecb3b9c43871c7a429a02f8423e621ab"
|
resolved "https://registry.yarnpkg.com/@vue/preload-webpack-plugin/-/preload-webpack-plugin-1.1.2.tgz#ceb924b4ecb3b9c43871c7a429a02f8423e621ab"
|
||||||
integrity sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==
|
integrity sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==
|
||||||
|
|
||||||
"@vue/shared@3.4.21":
|
"@vue/shared@3.4.23":
|
||||||
version "3.4.21"
|
version "3.4.23"
|
||||||
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1"
|
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.23.tgz#e536a6dfd2f5f950d08c2e8ebcfe7e5329a851a1"
|
||||||
integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==
|
integrity sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg==
|
||||||
|
|
||||||
"@vue/web-component-wrapper@^1.2.0":
|
"@vue/web-component-wrapper@^1.2.0":
|
||||||
version "1.3.0"
|
version "1.3.0"
|
||||||
@ -2712,6 +2712,13 @@ base@^0.11.1:
|
|||||||
mixin-deep "^1.2.0"
|
mixin-deep "^1.2.0"
|
||||||
pascalcase "^0.1.1"
|
pascalcase "^0.1.1"
|
||||||
|
|
||||||
|
basic-auth@^2.0.1:
|
||||||
|
version "2.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/basic-auth/-/basic-auth-2.0.1.tgz#b998279bf47ce38344b4f3cf916d4679bbf51e3a"
|
||||||
|
integrity sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==
|
||||||
|
dependencies:
|
||||||
|
safe-buffer "5.1.2"
|
||||||
|
|
||||||
batch@0.6.1:
|
batch@0.6.1:
|
||||||
version "0.6.1"
|
version "0.6.1"
|
||||||
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
resolved "https://registry.yarnpkg.com/batch/-/batch-0.6.1.tgz#dc34314f4e679318093fc760272525f94bf25c16"
|
||||||
@ -3121,9 +3128,9 @@ caniuse-api@^3.0.0:
|
|||||||
lodash.uniq "^4.5.0"
|
lodash.uniq "^4.5.0"
|
||||||
|
|
||||||
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001587:
|
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001587:
|
||||||
version "1.0.30001609"
|
version "1.0.30001612"
|
||||||
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz#fc34fad75c0c6d6d6303bdbceec2da8f203dabd6"
|
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz#d34248b4ec1f117b70b24ad9ee04c90e0b8a14ae"
|
||||||
integrity sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==
|
integrity sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==
|
||||||
|
|
||||||
case-sensitive-paths-webpack-plugin@^2.3.0:
|
case-sensitive-paths-webpack-plugin@^2.3.0:
|
||||||
version "2.4.0"
|
version "2.4.0"
|
||||||
@ -3576,9 +3583,9 @@ copy-webpack-plugin@^5.1.1:
|
|||||||
webpack-log "^2.0.0"
|
webpack-log "^2.0.0"
|
||||||
|
|
||||||
core-js-compat@^3.31.0, core-js-compat@^3.36.1, core-js-compat@^3.6.5:
|
core-js-compat@^3.31.0, core-js-compat@^3.36.1, core-js-compat@^3.6.5:
|
||||||
version "3.36.1"
|
version "3.37.0"
|
||||||
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.1.tgz#1818695d72c99c25d621dca94e6883e190cea3c8"
|
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73"
|
||||||
integrity sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==
|
integrity sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==
|
||||||
dependencies:
|
dependencies:
|
||||||
browserslist "^4.23.0"
|
browserslist "^4.23.0"
|
||||||
|
|
||||||
@ -3588,9 +3595,9 @@ core-js@^2.4.0:
|
|||||||
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
|
||||||
|
|
||||||
core-js@^3.6.5:
|
core-js@^3.6.5:
|
||||||
version "3.36.1"
|
version "3.37.0"
|
||||||
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578"
|
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.0.tgz#d8dde58e91d156b2547c19d8a4efd5c7f6c426bb"
|
||||||
integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==
|
integrity sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==
|
||||||
|
|
||||||
core-util-is@1.0.2:
|
core-util-is@1.0.2:
|
||||||
version "1.0.2"
|
version "1.0.2"
|
||||||
@ -4265,9 +4272,9 @@ ejs@^2.6.1:
|
|||||||
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
|
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
|
||||||
|
|
||||||
electron-to-chromium@^1.4.668:
|
electron-to-chromium@^1.4.668:
|
||||||
version "1.4.736"
|
version "1.4.745"
|
||||||
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.736.tgz#ecb4348f4d5c70fb1e31c347e5bad6b751066416"
|
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290"
|
||||||
integrity sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==
|
integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA==
|
||||||
|
|
||||||
elliptic@^6.5.3, elliptic@^6.5.5:
|
elliptic@^6.5.3, elliptic@^6.5.5:
|
||||||
version "6.5.5"
|
version "6.5.5"
|
||||||
@ -4810,6 +4817,13 @@ expand-brackets@^2.1.4:
|
|||||||
snapdragon "^0.8.1"
|
snapdragon "^0.8.1"
|
||||||
to-regex "^3.0.1"
|
to-regex "^3.0.1"
|
||||||
|
|
||||||
|
express-basic-auth@^1.2.1:
|
||||||
|
version "1.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/express-basic-auth/-/express-basic-auth-1.2.1.tgz#d31241c03a915dd55db7e5285573049cfcc36381"
|
||||||
|
integrity sha512-L6YQ1wQ/mNjVLAmK3AG1RK6VkokA1BIY6wmiH304Xtt/cLTps40EusZsU1Uop+v9lTDPxdtzbFmdXfFO3KEnwA==
|
||||||
|
dependencies:
|
||||||
|
basic-auth "^2.0.1"
|
||||||
|
|
||||||
express@^4.16.3, express@^4.17.1, express@^4.17.2:
|
express@^4.16.3, express@^4.17.1, express@^4.17.2:
|
||||||
version "4.19.2"
|
version "4.19.2"
|
||||||
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
|
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
|
||||||
@ -6789,10 +6803,10 @@ lru-cache@^6.0.0:
|
|||||||
dependencies:
|
dependencies:
|
||||||
yallist "^4.0.0"
|
yallist "^4.0.0"
|
||||||
|
|
||||||
magic-string@^0.30.7:
|
magic-string@^0.30.8:
|
||||||
version "0.30.9"
|
version "0.30.10"
|
||||||
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.9.tgz#8927ae21bfdd856310e07a1bc8dd5e73cb6c251d"
|
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
|
||||||
integrity sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==
|
integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
|
||||||
dependencies:
|
dependencies:
|
||||||
"@jridgewell/sourcemap-codec" "^1.4.15"
|
"@jridgewell/sourcemap-codec" "^1.4.15"
|
||||||
|
|
||||||
@ -8231,7 +8245,7 @@ postcss@^7.0.0, postcss@^7.0.1, postcss@^7.0.14, postcss@^7.0.27, postcss@^7.0.3
|
|||||||
picocolors "^0.2.1"
|
picocolors "^0.2.1"
|
||||||
source-map "^0.6.1"
|
source-map "^0.6.1"
|
||||||
|
|
||||||
postcss@^8.4.14, postcss@^8.4.35:
|
postcss@^8.4.14, postcss@^8.4.38:
|
||||||
version "8.4.38"
|
version "8.4.38"
|
||||||
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
|
||||||
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
integrity sha512-Wglpdk03BSfXkHoQa3b/oulrotAkwrlLDRSOb9D0bN86FdRyE9lppSp33aHNPgBa0JKCoB+drFLZkQoRRYae5A==
|
||||||
@ -9208,7 +9222,7 @@ source-list-map@^2.0.0:
|
|||||||
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
resolved "https://registry.yarnpkg.com/source-list-map/-/source-list-map-2.0.1.tgz#3993bd873bfc48479cca9ea3a547835c7c154b34"
|
||||||
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
|
integrity sha512-qnQ7gVMxGNxsiL4lEuJwe/To8UnK7fAnmbGEEH8RpLouuKbeEm0lhbQVFIrNSuB+G7tVrAlVsZgETT5nljf+Iw==
|
||||||
|
|
||||||
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.0.2, source-map-js@^1.2.0:
|
"source-map-js@>=0.6.2 <2.0.0", source-map-js@^1.2.0:
|
||||||
version "1.2.0"
|
version "1.2.0"
|
||||||
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
|
||||||
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==
|
||||||
|
Loading…
x
Reference in New Issue
Block a user