🔀 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:
Alicia Sykes 2024-04-28 22:58:57 +01:00 committed by GitHub
commit d92ae25700
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
41 changed files with 779 additions and 289 deletions

13
.env
View File

@ -27,6 +27,17 @@
# The path to the user data directory
# 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
# VUE_APP_CONFIG_PATH=/conf.yml
@ -52,7 +63,7 @@
# VUE_APP_VERSION=2.0.0
# 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
# VUE_APP_pihole_ip=http://your.pihole.ip

View File

@ -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>_"

View File

@ -42,7 +42,7 @@ RUN apk add --no-cache tzdata
COPY --from=BUILD_IMAGE /app ./
# Finally, run start command to serve up the built application
CMD [ "yarn", "build-and-start" ]
CMD [ "yarn", "start" ]
# Expose the port
EXPOSE ${PORT}

View File

@ -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>
</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]
> 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.

View File

@ -20,7 +20,7 @@ services:
# - /path/to/my-config.yml:/app/user-data/conf.yml
# - /path/to/item-icons:/app/user-data/item-icons/
# Set port that web service will be served on. Keep container port as 80
# Set port that web service will be served on. Keep container port as 8080
ports:
- 4000:8080

View File

@ -6,7 +6,10 @@
- [Logging In and Out](#logging-in-and-out)
- [Guest Access](#enabling-guest-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)
- [HTTP Auth](#http-auth)
- [Keycloak Auth](#keycloak)
- [Deploying Keycloak](#1-deploy-keycloak)
- [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.
### 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
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
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.

View File

@ -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.
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
- [`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
- [`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).
@ -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
- 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)
- 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).
@ -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).
@ -118,7 +144,7 @@ yarn build # Build 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`
---

View File

@ -1,6 +1,6 @@
{
"name": "dashy",
"version": "3.0.0",
"version": "3.0.1",
"license": "MIT",
"main": "server",
"author": "Alicia Sykes <alicia@omg.lol> (https://aliciasykes.com)",
@ -26,6 +26,7 @@
"connect-history-api-fallback": "^1.6.0",
"crypto-js": "^4.2.0",
"express": "^4.17.2",
"express-basic-auth": "^1.2.1",
"frappe-charts": "^1.6.2",
"js-yaml": "^4.1.0",
"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

View File

@ -50,6 +50,14 @@
<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">
/* Page Layout Styles */
body,
@ -60,7 +68,7 @@
}
body {
background: #141b33;
background: #0d1220;
display: flex;
flex-direction: column;
justify-content: center;
@ -194,15 +202,34 @@
}
.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>
<script>
const refreshRate = 8000;
// Refresh at interval
// Refresh the page every 10 seconds
const refreshRate = 10000;
setTimeout(() => { location.reload(); }, refreshRate);
// Get current stage
let initStage = parseInt(sessionStorage.getItem('initStage') || 0);
// Get current stage
let initStage = parseInt(sessionStorage.getItem('initStage') || 0);
// Check if stage in session storage is old, and if so, reset it
const now = Math.round(Date.now()/1000);
@ -262,4 +289,4 @@
</script>
</body>
</html>
</html>

View File

@ -6,14 +6,20 @@
* */
/* Import built-in Node server modules */
const fs = require('fs');
const os = require('os');
const dns = require('dns');
const http = require('http');
const path = require('path');
const util = require('util');
const dns = require('dns');
const os = require('os');
const crypto = require('crypto');
/* Import NPM dependencies */
const yaml = require('js-yaml');
/* Import Express + middleware functions */
const express = require('express');
const basicAuth = require('express-basic-auth');
const history = require('connect-history-api-fallback');
/* Kick of some basic checks */
@ -61,7 +67,7 @@ const printWelcomeMessage = () => {
console.log(printMessage(ip, port, isDocker)); // eslint-disable-line no-console
});
} 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
}
};
@ -71,6 +77,64 @@ const printWarning = (msg, error) => {
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 */
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 }));
}
})
// 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
.use(express.static(path.join(__dirname, process.env.USER_DATA_DIR || 'user-data')))
.use(express.static(path.join(__dirname, 'dist')))

View File

@ -11,7 +11,7 @@ const schema = require('../src/utils/ConfigSchema.json');
/* Tell AJV to use strict mode, and report all errors */
const validatorOptions = {
strict: true,
strict: false,
allowUnionTypes: true,
allErrors: true,
};

View File

@ -1,15 +1,15 @@
module.exports = (config, req) => {
try {
if ( config.appConfig.auth.enableHeaderAuth ) {
const userHeader = config.appConfig.auth.headerAuth.userHeader;
const proxyWhitelist = config.appConfig.auth.headerAuth.proxyWhitelist;
if ( proxyWhitelist.includes(req.socket.remoteAddress) ) {
return { "success": true, "user": req.headers[userHeader.toLowerCase()] };
if (config.appConfig.auth.enableHeaderAuth) {
const { userHeader } = config.appConfig.auth.headerAuth;
const { proxyWhitelist } = config.appConfig.auth.headerAuth;
if (proxyWhitelist.includes(req.socket.remoteAddress)) {
return { success: true, user: req.headers[userHeader.toLowerCase()] };
}
}
return {};
} catch (e) {
console.warn("Error get-user: ", e);
return { 'success': false };
console.warn('Error get-user: ', e);
return { success: false };
}
};
};

View File

@ -14,18 +14,23 @@ module.exports = async (newConfig, render) => {
return configObj.filename.replaceAll('/', '').replaceAll('..', '');
};
// Path to config file (with navigational characters stripped)
const usersFileName = makeSafeFileName(newConfig);
// Path to user data directory
const userDataDirectory = process.env.USER_DATA_DIR || './user-data/';
// Define constants for the config file
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',
filename: 'conf',
backupDenominator: '.backup.yml',
};
// Make the full file name and path to save the backup config file
const backupFilePath = `${path.normalize(process.env.BACKUP_DIR || settings.defaultLocation)
const backupFilePath = `${path.normalize(settings.backupLocation)
}/${usersFileName || settings.filename}-`
+ `${Math.round(new Date() / 1000)}${settings.backupDenominator}`;
@ -45,15 +50,20 @@ module.exports = async (newConfig, render) => {
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
.copyFile(defaultFilePath, backupFilePath)
.catch((error) => render(getRenderMessage(false, `Unable to backup ${settings.defaultFile}: ${error}`)));
.mkdir(settings.backupLocation, { recursive: true })
.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
await fsPromises
.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
await render(getRenderMessage(true));

View File

@ -4,6 +4,7 @@
<LoadingScreen :isLoading="isLoading" v-if="shouldShowSplash" />
<Header :pageInfo="pageInfo" />
<router-view v-if="!isFetching" />
<CriticalError v-if="hasCriticalError" />
<Footer :text="footerText" v-if="visibleComponents.footer && !isFetching" />
</div>
</template>
@ -12,6 +13,7 @@
import Header from '@/components/PageStrcture/Header.vue';
import Footer from '@/components/PageStrcture/Footer.vue';
import EditModeTopBanner from '@/components/InteractiveEditor/EditModeTopBanner.vue';
import CriticalError from '@/components/PageStrcture/CriticalError.vue';
import LoadingScreen from '@/components/PageStrcture/LoadingScreen.vue';
import { welcomeMsg } from '@/utils/CoolConsole';
import ErrorHandler from '@/utils/ErrorHandler';
@ -29,6 +31,7 @@ export default {
Footer,
LoadingScreen,
EditModeTopBanner,
CriticalError,
},
data() {
return {
@ -72,6 +75,9 @@ export default {
isEditMode() {
return this.$store.state.editMode;
},
hasCriticalError() {
return this.$store.state.criticalError;
},
subPageClassName() {
const currentSubPage = this.$store.state.currentConfigInfo;
return (currentSubPage && currentSubPage.pageId) ? currentSubPage.pageId : '';

View File

@ -312,6 +312,14 @@
"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": {
"general": {
"loading": "Loading...",

View File

@ -116,7 +116,8 @@ export default {
},
mounted() {
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;
if (!this.allowWriteToDisk) this.saveMode = 'local';
},

View File

@ -64,7 +64,6 @@ export default {
return this.$store.state.editMode;
},
sectionKey() {
if (this.isEditMode) return undefined;
return `collapsible-${this.uniqueKey}`;
},
collapseClass() {
@ -104,12 +103,23 @@ export default {
watch: {
checkboxState(newState) {
this.isExpanded = newState;
this.updateLocalStorage(); // Save every change immediately
},
uniqueKey() {
this.checkboxState = this.isExpanded;
uniqueKey(newVal, oldVal) {
if (newVal !== oldVal) {
this.refreshCollapseState(); // Refresh state when key changes
}
},
},
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 */
toggle() {
this.checkboxState = !this.checkboxState;

View 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>

View File

@ -37,8 +37,10 @@ export default {
input: '',
};
},
props: {
iconSize: String,
computed: {
iconSize() {
return this.$store.getters.iconSize;
},
},
components: {
IconSmall,

View File

@ -5,19 +5,19 @@
<IconDeafault
@click="updateDisplayLayout('auto')"
v-tooltip="tooltip($t('settings.layout-auto'))"
:class="`layout-icon ${displayLayout === 'auto' ? 'selected' : ''}`"
:class="`layout-icon ${layout === 'auto' ? 'selected' : ''}`"
tabindex="-2"
/>
<IconHorizontal
@click="updateDisplayLayout('horizontal')"
v-tooltip="tooltip($t('settings.layout-horizontal'))"
:class="`layout-icon ${displayLayout === 'horizontal' ? 'selected' : ''}`"
:class="`layout-icon ${layout === 'horizontal' ? 'selected' : ''}`"
tabindex="-2"
/>
<IconVertical
@click="updateDisplayLayout('vertical')"
v-tooltip="tooltip($t('settings.layout-vertical'))"
:class="`layout-icon ${displayLayout === 'vertical' ? 'selected' : ''}`"
:class="`layout-icon ${layout === 'vertical' ? 'selected' : ''}`"
tabindex="-2"
/>
</div>
@ -40,6 +40,11 @@ export default {
IconHorizontal,
IconVertical,
},
computed: {
layout() {
return this.$store.getters.layout;
},
},
methods: {
updateDisplayLayout(layout) {
this.$store.commit(StoreKeys.SET_ITEM_LAYOUT, layout);

View File

@ -7,7 +7,7 @@
<div class="options-outer">
<div :class="`options-container ${!settingsVisible ? 'hide' : ''}`">
<ThemeSelector />
<LayoutSelector :displayLayout="displayLayout" />
<LayoutSelector :displayLayout="$store.getters.layout" />
<ItemSizeSelector :iconSize="iconSize" />
<ConfigLauncher />
<AuthButtons v-if="userState !== 0" :userType="userState" />

View File

@ -1,9 +1,15 @@
<template>
<div class="readme-stats">
<img class="stats-card" v-if="!hideProfileCard" :src="profileCard" alt="Profile Card" />
<img class="stats-card" v-if="!hideLanguagesCard" :src="topLanguagesCard" alt="Languages" />
<a v-if="!hideProfileCard" :href="profileCardLink" target="_blank">
<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">
<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>
</div>
</template>
@ -61,6 +67,9 @@ export default {
profileCard() {
return `${widgetApiEndpoints.readMeStats}?username=${this.username}${this.cardConfig}`;
},
profileCardLink() {
return `https://github.com/${this.username}`;
},
topLanguagesCard() {
return `${widgetApiEndpoints.readMeStats}/top-langs/?username=${this.username}`
+ `${this.cardConfig}&langs_count=12`;
@ -70,8 +79,11 @@ export default {
this.repos.forEach((repo) => {
const username = repo.split('/')[0];
const repoName = repo.split('/')[1];
cards.push(`${widgetApiEndpoints.readMeStats}/pin/?username=${username}&repo=${repoName}`
+ `${this.cardConfig}&show_owner=true`);
cards.push({
cardSrc: `${widgetApiEndpoints.readMeStats}/pin/?username=${username}`
+ `&repo=${repoName}${this.cardConfig}&show_owner=true`,
cardHref: `https://github.com/${username}/${repoName}`,
});
});
return cards;
},

View File

@ -23,6 +23,7 @@ import { toastedOptions, tooltipOptions, language as defaultLanguage } from '@/u
import { initKeycloakAuth, isKeycloakEnabled } from '@/utils/KeycloakAuth';
import { initHeaderAuth, isHeaderAuthEnabled } from '@/utils/HeaderAuth';
import Keys from '@/utils/StoreMutations';
import ErrorHandler from '@/utils/ErrorHandler';
// Initialize global Vue components
Vue.use(VueI18n);
@ -61,16 +62,19 @@ const mount = () => new Vue({
}).$mount('#app');
store.dispatch(Keys.INITIALIZE_CONFIG).then(() => {
// Keycloak is enabled, redirect to KC login page
if (isKeycloakEnabled()) {
if (isKeycloakEnabled()) { // If Keycloak is enabled, initialize auth
initKeycloakAuth()
.then(() => mount())
.catch(() => window.location.reload());
} else if (isHeaderAuthEnabled()) {
.catch((e) => {
ErrorHandler('Failed to authenticate with Keycloak', e);
});
} else if (isHeaderAuthEnabled()) { // If header auth is enabled, initialize auth
initHeaderAuth()
.then(() => mount())
.catch(() => window.location.reload());
} else { // If Keycloak not enabled, then proceed straight to the app
.catch((e) => {
ErrorHandler('Failed to authenticate with server', e);
});
} else { // If no third-party auth, just mount the app as normal
mount();
}
});

View File

@ -28,19 +28,24 @@ const HomeMixin = {
return this.$store.state.modalOpen;
},
pageId() {
return (this.subPageInfo && this.subPageInfo.pageId) ? this.subPageInfo.pageId : 'home';
return this.$store.state.currentConfigInfo?.confId || 'home';
},
},
data: () => ({
searchValue: '',
}),
async mounted() {
// await this.getConfigForRoute();
},
watch: {
async $route() {
this.loadUpConfig();
},
pageInfo: {
handler(newPageInfo) {
if (newPageInfo && newPageInfo.title) {
document.title = newPageInfo.title;
}
},
immediate: true,
},
},
async created() {
this.loadUpConfig();
@ -79,6 +84,14 @@ const HomeMixin = {
searching(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 */
checkTheresData(sections) {
const localSections = localStorage[localStorageKeys.CONF_SECTIONS];

View File

@ -105,10 +105,18 @@ const WidgetMixin = {
const method = protocol || 'GET';
const url = this.useProxy ? this.proxyReqEndpoint : endpoint;
const data = JSON.stringify(body || {});
const CustomHeaders = options || {};
const headers = new Headers(this.useProxy
? ({ ...CustomHeaders, 'Target-URL': endpoint })
: CustomHeaders);
const headers = new Headers();
// 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
const bodyContent = method.toUpperCase() === 'GET' ? undefined : data;

View File

@ -8,7 +8,7 @@ import { makePageName, formatConfigPath, componentVisibility } from '@/utils/Con
import { applyItemId } from '@/utils/SectionHelpers';
import filterUserSections from '@/utils/CheckSectionVisibility';
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';
Vue.use(Vuex);
@ -41,8 +41,15 @@ const {
INSERT_ITEM,
UPDATE_CUSTOM_CSS,
CONF_MENU_INDEX,
CRITICAL_ERROR_MSG,
} = Keys;
const emptyConfig = {
appConfig: {},
pageInfo: { title: 'Dashy' },
sections: [],
};
const store = new Vuex.Store({
state: {
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
currentConfigInfo: {}, // For multi-page support, will store info about config file
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
},
getters: {
@ -106,7 +114,8 @@ const store = new Vuex.Store({
}
// Disable everything
if (appConfig.disableConfiguration
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())) {
|| (appConfig.disableConfigurationForNonAdmin && !isUserAdmin())
|| isLoggedInAsGuest()) {
perms.allowWriteToDisk = false;
perms.allowSaveLocally = false;
perms.allowViewConfig = false;
@ -137,10 +146,18 @@ const store = new Vuex.Store({
return foundSection;
},
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) {
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: {
@ -174,6 +191,10 @@ const store = new Vuex.Store({
state.editMode = editMode;
}
},
[CRITICAL_ERROR_MSG](state, message) {
if (message) ErrorHandler(message);
state.criticalError = message;
},
[UPDATE_ITEM](state, payload) {
const { itemId, newItem } = payload;
const newConfig = { ...state.config };
@ -298,11 +319,23 @@ const store = new Vuex.Store({
InfoHandler('Color palette updated', InfoKeys.VISUAL);
},
[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);
},
[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);
},
[UPDATE_CUSTOM_CSS](state, customCss) {
@ -320,16 +353,39 @@ const store = new Vuex.Store({
actions: {
/* Fetches the root config file, only ever called by INITIALIZE_CONFIG */
async [INITIALIZE_ROOT_CONFIG]({ commit }) {
// Load and parse config from root config file
const configFilePath = process.env.VUE_APP_CONFIG_PATH || '/conf.yml';
const data = await yaml.load((await axios.get(configFilePath)).data);
// 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);
return data;
try {
// Attempt to fetch the YAML file
const response = await axios.get(configFilePath, makeBasicAuthHeaders());
let data;
try {
data = yaml.load(response.data);
} catch (parseError) {
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
@ -339,6 +395,7 @@ const store = new Vuex.Store({
*/
async [INITIALIZE_CONFIG]({ commit, state }, subConfigId) {
const rootConfig = state.rootConfig || await this.dispatch(Keys.INITIALIZE_ROOT_CONFIG);
commit(SET_IS_USING_LOCAL_CONFIG, false);
if (!subConfigId) { // Use root config as config
commit(SET_CONFIG, rootConfig);
@ -351,7 +408,7 @@ const store = new Vuex.Store({
const json = JSON.parse(localSectionsRaw);
if (json.length >= 1) localSections = json;
} catch (e) {
ErrorHandler('Malformed section data in local storage');
commit(CRITICAL_ERROR_MSG, 'Malformed section data in local storage');
}
}
if (localSections.length > 0) {
@ -366,11 +423,10 @@ const store = new Vuex.Store({
)?.path);
if (!subConfigPath) {
ErrorHandler(`Unable to find config for '${subConfigId}'`);
return null;
commit(CRITICAL_ERROR_MSG, `Unable to find config for '${subConfigId}'`);
return { ...emptyConfig };
}
axios.get(subConfigPath).then((response) => {
axios.get(subConfigPath, makeBasicAuthHeaders()).then((response) => {
// Parse the YAML
const configContent = yaml.load(response.data) || {};
// Certain values must be inherited from root config
@ -389,17 +445,17 @@ const store = new Vuex.Store({
commit(SET_IS_USING_LOCAL_CONFIG, true);
}
} 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
commit(SET_CONFIG, configContent);
commit(SET_CURRENT_CONFIG_INFO, { confPath: subConfigPath, confId: subConfigId });
}).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: {},

View File

@ -31,6 +31,7 @@
--transparent-white-70: #ffffffb3;
--transparent-white-50: #ffffff80;
--transparent-white-30: #ffffff4d;
--transparent-white-10: #ffffff0f;
/* Color variables for specific components
* all variables are optional, since they inherit initial values from above*

View File

@ -1717,6 +1717,7 @@ html[data-theme='neomorphic'] {
.config-buttons > svg,
.display-options svg,
form.minimal input,
.critical-error-wrap button.user-doesnt-care,
a.config-button, button.config-button {
border-radius: 0.35rem;
box-shadow: var(--glass-button-shadow);
@ -1724,6 +1725,7 @@ html[data-theme='neomorphic'] {
border: 1px solid rgba(255, 255, 255, 0.19);
background: rgba(255, 255, 255, 0.15);
transition: all 0.2s ease-in-out;
text-decoration: none;
&:hover, &.selected {
box-shadow: var(--glass-button-hover-shadow);
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);
backdrop-filter: blur(50px);
}
.critical-error-wrap {
backdrop-filter: blur(15px);
background: #0f0528c4;
}
}
html[data-theme='glass'] {

View File

@ -22,6 +22,11 @@ html {
}
}
#dashy {
position: relative;
min-height: 100vh;
}
/* Hide text, and show 'Loading...' while Vue is initializing tags */
[v-cloak] > * { display:none }
[v-cloak]::before { content: "loading…" }

View File

@ -11,8 +11,6 @@ const getAppConfig = () => {
return config.appConfig || {};
};
// const appConfig = $store.getters.appConfig || {};
/**
* Called when the user is still using array for users, prints warning
* This was a breaking change, implemented in V 1.6.5
@ -41,37 +39,52 @@ const getUsers = () => {
* @returns {String} The hashed token
*/
const generateUserToken = (user) => {
if (!user.user || !user.hash) {
ErrorHandler('Invalid user object. Must have `user` and `hash` parameters');
if (!user.user || (!user.hash && !user.password)) {
ErrorHandler('Invalid user object. Must have `user` and either a `hash` or `password` param');
return undefined;
}
const passHash = user.hash || sha256(process.env[user.password]).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);
};
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
* @returns {Boolean} Will return true if the user is logged in, else false
*/
export const isLoggedIn = () => {
const users = getUsers();
let userAuthenticated = document.cookie.split(';').some((cookie) => {
if (cookie && cookie.split('=').length > 1) {
const cookieKey = cookie.split('=')[0].trim();
const cookieValue = cookie.split('=')[1].trim();
if (cookieKey === cookieKeys.AUTH_TOKEN) {
userAuthenticated = users.some((user) => {
if (generateUserToken(user) === cookieValue) {
localStorage.setItem(localStorageKeys.USERNAME, user.user);
return true;
} else return false;
});
return userAuthenticated;
} else return false;
const cookieToken = getCookieToken();
return users.some((user) => {
if (generateUserToken(user) === cookieToken) {
localStorage.setItem(localStorageKeys.USERNAME, user.user);
return true;
} else return false;
});
return userAuthenticated;
};
/* Returns true if authentication is enabled */
@ -108,7 +121,18 @@ export const checkCredentials = (username, pass, users, messages) => {
} else {
users.forEach((user) => {
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
} else { // User found, but password is not a match
response = { correct: false, msg: messages.incorrectPassword };
@ -163,9 +187,9 @@ export const getCurrentUser = () => {
* Checks if the user is viewing the dashboard as a guest
* Returns true if guest mode enabled, and user not logged in
* */
export const isLoggedInAsGuest = (currentUser) => {
export const isLoggedInAsGuest = () => {
const guestEnabled = isGuestAccessEnabled();
const loggedIn = isLoggedIn() && currentUser;
const loggedIn = isLoggedIn();
return guestEnabled && !loggedIn;
};

View File

@ -500,14 +500,9 @@
"users": {
"title": "Users",
"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": {
"type": "object",
"additionalProperties": false,
"required": [
"user",
"hash"
],
"properties": {
"user": {
"title": "Username",
@ -521,6 +516,12 @@
"minLength": 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": {
"title": "Privileges",
"type": "string",
@ -531,9 +532,15 @@
"description": "User type, denoting privilege level, either admin or normal",
"default": "normal"
}
}
},
"additionalProperties": false,
"required": ["user"],
"oneOf": [
{ "required": ["hash"] },
{ "required": ["password"] }
]
}
},
},
"enableHeaderAuth": {
"title": "Enable HeaderAuth?",
"type": "boolean",

View File

@ -3,7 +3,7 @@ import sha256 from 'crypto-js/sha256';
import ConfigAccumulator from '@/utils/ConfigAccumalator';
import { cookieKeys, localStorageKeys, serviceEndpoints } from '@/utils/defaults';
import { InfoHandler, ErrorHandler, InfoKeys } from '@/utils/ErrorHandler';
import { logout } from '@/utils/Auth';
import { logout as authLogout } from '@/utils/Auth';
const getAppConfig = () => {
const Accumulator = new ConfigAccumulator();
@ -22,7 +22,6 @@ class HeaderAuth {
this.users = auth.users;
}
/* eslint-disable class-methods-use-this */
login() {
return new Promise((resolve, reject) => {
const baseUrl = process.env.VUE_APP_DOMAIN || window.location.origin;
@ -44,6 +43,7 @@ class HeaderAuth {
}
});
} catch (e) {
ErrorHandler('Error while trying to login using header authentication', e);
reject(e);
}
}
@ -51,8 +51,9 @@ class HeaderAuth {
});
}
// eslint-disable-next-line class-methods-use-this
logout() {
logout();
authLogout();
}
}

View File

@ -27,7 +27,7 @@ const determineIntersection = (source = [], target = []) => {
/* Returns false if the displayData of a section/item
should not be rendered for the current user/ guest */
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
const checkVisibility = () => {

View File

@ -29,6 +29,7 @@ const KEY_NAMES = [
'INSERT_ITEM',
'UPDATE_CUSTOM_CSS',
'CONF_MENU_INDEX',
'CRITICAL_ERROR_MSG',
];
// Convert array of key names into an object, and export

View File

@ -135,6 +135,7 @@ module.exports = {
MOST_USED: 'mostUsed',
LAST_USED: 'lastUsed',
KEYCLOAK_INFO: 'keycloakInfo',
DISABLE_CRITICAL_WARNING: 'disableCriticalWarning',
},
/* Key names for cookie identifiers */
cookieKeys: {

View File

@ -19,14 +19,7 @@
</router-link>
</div>
<!-- Main content, section for each group of items -->
<div v-if="checkTheresData(sections) || isEditMode"
:class="`item-group-container `
+ `orientation-${layout} `
+ `item-size-${itemSizeBound} `
+ (isEditMode ? 'edit-mode ' : '')
+ (singleSectionView ? 'single-section-view ' : '')
+ (this.colCount ? `col-count-${this.colCount} ` : '')"
>
<div v-if="checkTheresData(sections) || isEditMode" :class="computedClass">
<template v-for="(section, index) in filteredSections">
<Section
:key="index"
@ -34,7 +27,7 @@
:title="section.name"
:icon="section.icon || undefined"
:displayData="getDisplayData(section)"
:groupId="`${pageId}-section-${index}`"
:groupId="makeSectionId(section)"
:items="section.filteredItems"
:widgets="section.widgets"
:searchTerm="searchValue"
@ -70,7 +63,7 @@ import ExportConfigMenu from '@/components/InteractiveEditor/ExportConfigMenu.vu
import AddNewSection from '@/components/InteractiveEditor/AddNewSectionLauncher.vue';
import NotificationThing from '@/components/Settings/LocalConfigWarning.vue';
import StoreKeys from '@/utils/StoreMutations';
import { localStorageKeys, modalNames } from '@/utils/defaults';
import { modalNames } from '@/utils/defaults';
import ErrorHandler from '@/utils/ErrorHandler';
import BackIcon from '@/assets/interface-icons/back-arrow.svg';
@ -120,19 +113,13 @@ export default {
iconSize() {
return this.$store.getters.iconSize;
},
},
watch: {
layoutOrientation(layout) {
if (layout) {
localStorage.setItem(localStorageKeys.LAYOUT_ORIENTATION, layout);
this.layout = layout;
}
},
iconSize(size) {
if (size) {
localStorage.setItem(localStorageKeys.ICON_SIZE, size);
this.itemSizeBound = size;
}
computedClass() {
let classes = 'item-group-container '
+ ` orientation-${this.$store.getters.layout} item-size-${this.itemSizeBound}`;
if (this.isEditMode) classes += ' edit-mode';
if (this.singleSectionView) classes += ' single-section-view';
if (this.colCount) classes += ` col-count-${this.colCount}`;
return classes;
},
},
methods: {

View File

@ -34,7 +34,7 @@
:index="index"
:title="section.name"
:icon="section.icon || undefined"
:groupId="`section-${index}`"
:groupId="makeSectionId(section)"
:items="filterTiles(section.items)"
:widgets="section.widgets"
:selected="selectedSection === index"

264
yarn.lock
View File

@ -1179,93 +1179,93 @@
mkdirp "^1.0.4"
rimraf "^3.0.2"
"@sentry-internal/feedback@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.110.0.tgz#7103a08cd6bfb43583087d7476a5f24c5857cc22"
integrity sha512-hrfWa3WkSOiBO5Srcr1j4kuGOlbsQic+REpLOofllVIs56DOo9+Aj9svxT+dcvZERv/nlFSV/E0BfGy9g08IEg==
"@sentry-internal/feedback@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/feedback/-/feedback-7.111.0.tgz#c715e7e6a1877b60cd1f4dff85969660e0deff3f"
integrity sha512-xaKgPPDEirOan7c9HwzYA1KK87kRp/qfIx9ZKLOEtxwy6nqoMuSByGqSwm1Oqfcjpbd7y6/y+7Bw+69ZKNVLDQ==
dependencies:
"@sentry/core" "7.110.0"
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry/core" "7.111.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sentry-internal/replay-canvas@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.110.0.tgz#af21b56157f44c44a2eedf4326ef37f4ea440fa8"
integrity sha512-SNa+AfyfX+vc6Xw0pIfDsa5Qnc9cpexU6M2D19gadtVhmep7qoFBuhBVZrSv6BtdCxvrb5EyYsHYGfjQdIDcvg==
"@sentry-internal/replay-canvas@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/replay-canvas/-/replay-canvas-7.111.0.tgz#aa3cba0477f312cbf40eff4eabeaeda6221a55b6"
integrity sha512-3KPBIpiegTYmuVw9gA2aKuliAQONS3Ny1kJc9x5kz6XQGuLFxqlh6KzoCVaKfQJeq2WJqRNeR4KFFuNGuB3H8w==
dependencies:
"@sentry/core" "7.110.0"
"@sentry/replay" "7.110.0"
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry/core" "7.111.0"
"@sentry/replay" "7.111.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sentry-internal/tracing@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.110.0.tgz#00f2086b0efb8dd5a67831074e52b176aa542d32"
integrity sha512-IIHHa9e/mE7uOMJfNELI8adyoELxOy6u6TNCn5t6fphmq84w8FTc9adXkG/FY2AQpglkIvlILojfMROFB2aaAQ==
"@sentry-internal/tracing@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry-internal/tracing/-/tracing-7.111.0.tgz#b352df9f38009c5d306308a829a1dd9a57f084fd"
integrity sha512-CgXly8rsdu4loWVKi2RqpInH3C2cVBuaYsx4ZP5IJpzSinsUAMyyr3Pc0PZzCyoVpBBXGBGj/4HhFsY3q6Z0Vg==
dependencies:
"@sentry/core" "7.110.0"
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry/core" "7.111.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sentry/browser@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.110.0.tgz#40900d76a8f423a7163a594ec9267a2e0ebd8a5b"
integrity sha512-gIxedVm6ZgkjQfgCDgLWJgAsolq6OxV8hQ2j1+RaDL2RngvelFo/vlX5f2sD6EbjVp77Cri8u5GkMJF+v4p84g==
"@sentry/browser@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/browser/-/browser-7.111.0.tgz#29da73e7192eb5643d101c47922d7374e4cc88ed"
integrity sha512-x7S9XoJh+TbMnur4eBhPpCVo+p7udABBV2gQk+Iw6LP9e8EFKmGmNyl76vSsT6GeFJ7mwxDEKfuwbVoLBjIvHw==
dependencies:
"@sentry-internal/feedback" "7.110.0"
"@sentry-internal/replay-canvas" "7.110.0"
"@sentry-internal/tracing" "7.110.0"
"@sentry/core" "7.110.0"
"@sentry/replay" "7.110.0"
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry-internal/feedback" "7.111.0"
"@sentry-internal/replay-canvas" "7.111.0"
"@sentry-internal/tracing" "7.111.0"
"@sentry/core" "7.111.0"
"@sentry/replay" "7.111.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sentry/core@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.110.0.tgz#2945d3ac0ef116ed313fbfb9da4f483b66fe5bca"
integrity sha512-g4suCQO94mZsKVaAbyD1zLFC5YSuBQCIPHXx9fdgtfoPib7BWjWWePkllkrvsKAv4u8Oq05RfnKOhOMRHpOKqg==
"@sentry/core@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/core/-/core-7.111.0.tgz#54c9037a3b79b3623377dce1887b69b40670e201"
integrity sha512-/ljeMjZu8CSrLGrseBi/7S2zRIFsqMcvfyG6Nwgfc07J9nbHt8/MqouE1bXZfiaILqDBpK7BK9MLAAph4mkAWg==
dependencies:
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sentry/replay@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.110.0.tgz#e185c88cec573724b46b79ada7ef5a7098acd1b6"
integrity sha512-EEpGPf3iBJjWejvoxKLVMnLtLNwPTUxHJV1oxUkbcSi3B/tG5hW7LArYDjAcvkfa4VmA8JLCwj2vYU5MQ8tj6g==
"@sentry/replay@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/replay/-/replay-7.111.0.tgz#6d21bddf2ec245db6eb2c471e81efd94364107ae"
integrity sha512-cSbI4A4hrO0sZ0ynvLQauPg8YyaDOQkhGkyvbws8W9WgfxR8X827bY9S0f1TPfgaFiVcKb0iRaAwyXHg3pyzOg==
dependencies:
"@sentry-internal/tracing" "7.110.0"
"@sentry/core" "7.110.0"
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry-internal/tracing" "7.111.0"
"@sentry/core" "7.111.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sentry/tracing@^7.102.1":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.110.0.tgz#9e8836babba9894309d337f3006b98d79b863329"
integrity sha512-pAydcCqzyzn2Uv9qmuDX5saHbXp4eMMsBW2C/oSkVdKQQSdA7JeG27d82Jz3cMVrfjv105lShP5qS2YjhBTkow==
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/tracing/-/tracing-7.111.0.tgz#3c9da76e6ca1f2e17e138ce828330e93e53f67a4"
integrity sha512-+BHvdCJxcNnBkru3Y5aFZssEwyNU/mwPTSZqYOhFilokVIrDmVrP/R9g8jHSUqXF4KwB3RaknTPj/4484Z0erA==
dependencies:
"@sentry-internal/tracing" "7.110.0"
"@sentry-internal/tracing" "7.111.0"
"@sentry/types@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.110.0.tgz#c3f252b008cab905097fc71e174191f20bdaf4f3"
integrity sha512-DqYBLyE8thC5P5MuPn+sj8tL60nCd/f5cerFFPcudn5nJ4Zs1eI6lKlwwyHYTEu5c4KFjCB0qql6kXfwAHmTyA==
"@sentry/types@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/types/-/types-7.111.0.tgz#9c869c3c51d606041916765ba58f29de915707ac"
integrity sha512-Oti4pgQ55+FBHKKcHGu51ZUxO1u52G5iVNK4mbtAN+5ArSCy/2s1H8IDJiOMswn3acfUnCR0oB/QsbEgAPZ26g==
"@sentry/utils@7.110.0":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.110.0.tgz#68ef59359d608a1a6a7205b780196a042ad73ab2"
integrity sha512-VBsdLLN+5tf73fhf/Cm7JIsUJ6y9DkJj8h4I6Mxx0rszrvOyH6S5px40K+V4jdLBzMEvVinC7q2Cbf1YM18BSw==
"@sentry/utils@7.111.0":
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/utils/-/utils-7.111.0.tgz#e006cc1e751b30ff5cf914c34eb143102e2e8c2d"
integrity sha512-CB5rz1EgCSwj3xoXogsCZ5pQtfERrURc/ItcCuoaijUhkD0iMq5MCNWMHW3mBsBrqx/Oba+XGvDu0t/5+SWwBg==
dependencies:
"@sentry/types" "7.110.0"
"@sentry/types" "7.111.0"
"@sentry/vue@^7.102.1":
version "7.110.0"
resolved "https://registry.yarnpkg.com/@sentry/vue/-/vue-7.110.0.tgz#60bca341bf55c0ebe3bbd5f220241f5e34adbb8b"
integrity sha512-N8qAAPNJMV9fRMfvbRIWgFrn+wNH6ABGdc7fbFg1y3y0rOw58YMMg0+WdHMGEeWhH7N2/cCJGUHdz4egqaM3gQ==
version "7.111.0"
resolved "https://registry.yarnpkg.com/@sentry/vue/-/vue-7.111.0.tgz#064e1523fc97a81e0a62fc9c40bd905e975fb4f0"
integrity sha512-MEvv+1r7548rMuZF3WbxY2OYxHyjuROMTptYR2xrQj+jEkJ1hFbZyn5J+uH/9OamGY2rksnMqxFBcnfdqrItvA==
dependencies:
"@sentry/browser" "7.110.0"
"@sentry/core" "7.110.0"
"@sentry/types" "7.110.0"
"@sentry/utils" "7.110.0"
"@sentry/browser" "7.111.0"
"@sentry/core" "7.111.0"
"@sentry/types" "7.111.0"
"@sentry/utils" "7.111.0"
"@sideway/address@^4.1.5":
version "4.1.5"
@ -1336,9 +1336,9 @@
"@types/estree" "*"
"@types/eslint@*":
version "8.56.9"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.9.tgz#403e9ced04a34e63f1c383c5b8ee1a94442c8cc4"
integrity sha512-W4W3KcqzjJ0sHg2vAq9vfml6OhsJ53TcUjUqfzzZf/EChUtwspszj/S0pzMxnfRcO55/iGq47dscXw71Fxc4Zg==
version "8.56.10"
resolved "https://registry.yarnpkg.com/@types/eslint/-/eslint-8.56.10.tgz#eb2370a73bf04a901eeba8f22595c7ee0f7eb58d"
integrity sha512-Shavhk87gCtY2fhXDctcfS3e6FdxWkCx1iUZ9eEUbh7rTqlZT0/IzOkCOVt0fCjcFuZ9FPYfuezTBImfHCDBGQ==
dependencies:
"@types/estree" "*"
"@types/json-schema" "*"
@ -1436,9 +1436,9 @@
integrity sha512-hroOstUScF6zhIi+5+x0dzqrHA1EJi+Irri6b1fxolMTqqHIV/Cg77EtnQcZqZCu8hR3mX2BzIxN4/GzI68Kfw==
"@types/qs@*":
version "6.9.14"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.14.tgz#169e142bfe493895287bee382af6039795e9b75b"
integrity sha512-5khscbd3SwWMhFqylJBLQ0zIu7c1K6Vz0uBIt915BI3zV0q1nfjRQD3RqSBcPaO6PHEF4ov/t9y89fSiyThlPA==
version "6.9.15"
resolved "https://registry.yarnpkg.com/@types/qs/-/qs-6.9.15.tgz#adde8a060ec9c305a82de1babc1056e73bd64dce"
integrity sha512-uXHQKES6DQKKCLh441Xv/dwxOq1TVS3JPUMlEqoEglvlhR6Mxnlew/Xq/LRVHpLyk7iK3zODe1qYHIMltO7XGg==
"@types/range-parser@*":
version "1.2.7"
@ -1816,24 +1816,24 @@
semver "^7.3.4"
strip-ansi "^6.0.0"
"@vue/compiler-core@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.21.tgz#868b7085378fc24e58c9aed14c8d62110a62be1a"
integrity sha512-MjXawxZf2SbZszLPYxaFCjxfibYrzr3eYbKxwpLR9EQN+oaziSu3qKVbwBERj1IFIB8OLUewxB5m/BFzi613og==
"@vue/compiler-core@3.4.23":
version "3.4.23"
resolved "https://registry.yarnpkg.com/@vue/compiler-core/-/compiler-core-3.4.23.tgz#a08f5998e391ad75e602a66dd7255af9054df2f3"
integrity sha512-HAFmuVEwNqNdmk+w4VCQ2pkLk1Vw4XYiiyxEp3z/xvl14aLTUBw2OfVH3vBcx+FtGsynQLkkhK410Nah1N2yyQ==
dependencies:
"@babel/parser" "^7.23.9"
"@vue/shared" "3.4.21"
"@babel/parser" "^7.24.1"
"@vue/shared" "3.4.23"
entities "^4.5.0"
estree-walker "^2.0.2"
source-map-js "^1.0.2"
source-map-js "^1.2.0"
"@vue/compiler-dom@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.21.tgz#0077c355e2008207283a5a87d510330d22546803"
integrity sha512-IZC6FKowtT1sl0CR5DpXSiEB5ayw75oT2bma1BEhV7RRR1+cfwLrxc2Z8Zq/RGFzJ8w5r9QtCOvTjQgdn0IKmA==
"@vue/compiler-dom@3.4.23":
version "3.4.23"
resolved "https://registry.yarnpkg.com/@vue/compiler-dom/-/compiler-dom-3.4.23.tgz#6fa622d1e5c8508551564c5dc5948e9cddf60867"
integrity sha512-t0b9WSTnCRrzsBGrDd1LNR5HGzYTr7LX3z6nNBG+KGvZLqrT0mY6NsMzOqlVMBKKXKVuusbbB5aOOFgTY+senw==
dependencies:
"@vue/compiler-core" "3.4.21"
"@vue/shared" "3.4.21"
"@vue/compiler-core" "3.4.23"
"@vue/shared" "3.4.23"
"@vue/compiler-sfc@2.7.16":
version "2.7.16"
@ -1847,27 +1847,27 @@
prettier "^1.18.2 || ^2.0.0"
"@vue/compiler-sfc@^3.4.15":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.21.tgz#4af920dc31ab99e1ff5d152b5fe0ad12181145b2"
integrity sha512-me7epoTxYlY+2CUM7hy9PCDdpMPfIwrOvAXud2Upk10g4YLv9UBW7kL798TvMeDhPthkZ0CONNrK2GoeI1ODiQ==
version "3.4.23"
resolved "https://registry.yarnpkg.com/@vue/compiler-sfc/-/compiler-sfc-3.4.23.tgz#7041517b9bbd1b304f0db33bfa424e9a899fda8d"
integrity sha512-fSDTKTfzaRX1kNAUiaj8JB4AokikzStWgHooMhaxyjZerw624L+IAP/fvI4ZwMpwIh8f08PVzEnu4rg8/Npssw==
dependencies:
"@babel/parser" "^7.23.9"
"@vue/compiler-core" "3.4.21"
"@vue/compiler-dom" "3.4.21"
"@vue/compiler-ssr" "3.4.21"
"@vue/shared" "3.4.21"
"@babel/parser" "^7.24.1"
"@vue/compiler-core" "3.4.23"
"@vue/compiler-dom" "3.4.23"
"@vue/compiler-ssr" "3.4.23"
"@vue/shared" "3.4.23"
estree-walker "^2.0.2"
magic-string "^0.30.7"
postcss "^8.4.35"
source-map-js "^1.0.2"
magic-string "^0.30.8"
postcss "^8.4.38"
source-map-js "^1.2.0"
"@vue/compiler-ssr@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.21.tgz#b84ae64fb9c265df21fc67f7624587673d324fef"
integrity sha512-M5+9nI2lPpAsgXOGQobnIueVqc9sisBFexh5yMIMRAPYLa7+5wEJs8iqOZc1WAa9WQbx9GR2twgznU8LTIiZ4Q==
"@vue/compiler-ssr@3.4.23":
version "3.4.23"
resolved "https://registry.yarnpkg.com/@vue/compiler-ssr/-/compiler-ssr-3.4.23.tgz#1ae4afe962a9e156b1a79eff909c37cd423dd4c2"
integrity sha512-hb6Uj2cYs+tfqz71Wj6h3E5t6OKvb4MVcM2Nl5i/z1nv1gjEhw+zYaNOV+Xwn+SSN/VZM0DgANw5TuJfxfezPg==
dependencies:
"@vue/compiler-dom" "3.4.21"
"@vue/shared" "3.4.21"
"@vue/compiler-dom" "3.4.23"
"@vue/shared" "3.4.23"
"@vue/component-compiler-utils@^3.1.0", "@vue/component-compiler-utils@^3.1.2":
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"
integrity sha512-LIZMuJk38pk9U9Ur4YzHjlIyMuxPlACdBIHH9/nGYVTsaGKOSnSuELiE8vS9wa+dJpIYspYUOqk+L1Q4pgHQHQ==
"@vue/shared@3.4.21":
version "3.4.21"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.21.tgz#de526a9059d0a599f0b429af7037cd0c3ed7d5a1"
integrity sha512-PuJe7vDIi6VYSinuEbUIQgMIRZGgM8e4R+G+/dQTk0X1NEdvgvvgv7m+rfmDH1gZzyA1OjjoWskvHlfRNfQf3g==
"@vue/shared@3.4.23":
version "3.4.23"
resolved "https://registry.yarnpkg.com/@vue/shared/-/shared-3.4.23.tgz#e536a6dfd2f5f950d08c2e8ebcfe7e5329a851a1"
integrity sha512-wBQ0gvf+SMwsCQOyusNw/GoXPV47WGd1xB5A1Pgzy0sQ3Bi5r5xm3n+92y3gCnB3MWqnRDdvfkRGxhKtbBRNgg==
"@vue/web-component-wrapper@^1.2.0":
version "1.3.0"
@ -2712,6 +2712,13 @@ base@^0.11.1:
mixin-deep "^1.2.0"
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:
version "0.6.1"
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"
caniuse-lite@^1.0.0, caniuse-lite@^1.0.30001109, caniuse-lite@^1.0.30001587:
version "1.0.30001609"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001609.tgz#fc34fad75c0c6d6d6303bdbceec2da8f203dabd6"
integrity sha512-JFPQs34lHKx1B5t1EpQpWH4c+29zIyn/haGsbpfq3suuV9v56enjFt23zqijxGTMwy1p/4H2tjnQMY+p1WoAyA==
version "1.0.30001612"
resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30001612.tgz#d34248b4ec1f117b70b24ad9ee04c90e0b8a14ae"
integrity sha512-lFgnZ07UhaCcsSZgWW0K5j4e69dK1u/ltrL9lTUiFOwNHs12S3UMIEYgBV0Z6C6hRDev7iRnMzzYmKabYdXF9g==
case-sensitive-paths-webpack-plugin@^2.3.0:
version "2.4.0"
@ -3576,9 +3583,9 @@ copy-webpack-plugin@^5.1.1:
webpack-log "^2.0.0"
core-js-compat@^3.31.0, core-js-compat@^3.36.1, core-js-compat@^3.6.5:
version "3.36.1"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.36.1.tgz#1818695d72c99c25d621dca94e6883e190cea3c8"
integrity sha512-Dk997v9ZCt3X/npqzyGdTlq6t7lDBhZwGvV94PKzDArjp7BTRm7WlDAXYd/OWdeFHO8OChQYRJNJvUCqCbrtKA==
version "3.37.0"
resolved "https://registry.yarnpkg.com/core-js-compat/-/core-js-compat-3.37.0.tgz#d9570e544163779bb4dff1031c7972f44918dc73"
integrity sha512-vYq4L+T8aS5UuFg4UwDhc7YNRWVeVZwltad9C/jV3R2LgVOpS9BDr7l/WL6BN0dbV3k1XejPTHqqEzJgsa0frA==
dependencies:
browserslist "^4.23.0"
@ -3588,9 +3595,9 @@ core-js@^2.4.0:
integrity sha512-Kb2wC0fvsWfQrgk8HU5lW6U/Lcs8+9aaYcy4ZFc6DDlo4nZ7n70dEgE5rtR0oG6ufKDUnrwfWL1mXR5ljDatrQ==
core-js@^3.6.5:
version "3.36.1"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.36.1.tgz#c97a7160ebd00b2de19e62f4bbd3406ab720e578"
integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==
version "3.37.0"
resolved "https://registry.yarnpkg.com/core-js/-/core-js-3.37.0.tgz#d8dde58e91d156b2547c19d8a4efd5c7f6c426bb"
integrity sha512-fu5vHevQ8ZG4og+LXug8ulUtVxjOcEYvifJr7L5Bfq9GOztVqsKd9/59hUk2ZSbCrS3BqUr3EpaYGIYzq7g3Ug==
core-util-is@1.0.2:
version "1.0.2"
@ -4265,9 +4272,9 @@ ejs@^2.6.1:
integrity sha512-7vmuyh5+kuUyJKePhQfRQBhXV5Ce+RnaeeQArKu1EAMpL3WbgMt5WG6uQZpEVvYSSsxMXRKOewtDk9RaTKXRlA==
electron-to-chromium@^1.4.668:
version "1.4.736"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.736.tgz#ecb4348f4d5c70fb1e31c347e5bad6b751066416"
integrity sha512-Rer6wc3ynLelKNM4lOCg7/zPQj8tPOCB2hzD32PX9wd3hgRRi9MxEbmkFCokzcEhRVMiOVLjnL9ig9cefJ+6+Q==
version "1.4.745"
resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.4.745.tgz#9c202ce9cbf18a5b5e0ca47145fd127cc4dd2290"
integrity sha512-tRbzkaRI5gbUn5DEvF0dV4TQbMZ5CLkWeTAXmpC9IrYT+GE+x76i9p+o3RJ5l9XmdQlI1pPhVtE9uNcJJ0G0EA==
elliptic@^6.5.3, elliptic@^6.5.5:
version "6.5.5"
@ -4810,6 +4817,13 @@ expand-brackets@^2.1.4:
snapdragon "^0.8.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:
version "4.19.2"
resolved "https://registry.yarnpkg.com/express/-/express-4.19.2.tgz#e25437827a3aa7f2a827bc8171bbbb664a356465"
@ -6789,10 +6803,10 @@ lru-cache@^6.0.0:
dependencies:
yallist "^4.0.0"
magic-string@^0.30.7:
version "0.30.9"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.9.tgz#8927ae21bfdd856310e07a1bc8dd5e73cb6c251d"
integrity sha512-S1+hd+dIrC8EZqKyT9DstTH/0Z+f76kmmvZnkfQVmOpDEF9iVgdYif3Q/pIWHmCoo59bQVGW0kVL3e2nl+9+Sw==
magic-string@^0.30.8:
version "0.30.10"
resolved "https://registry.yarnpkg.com/magic-string/-/magic-string-0.30.10.tgz#123d9c41a0cb5640c892b041d4cfb3bd0aa4b39e"
integrity sha512-iIRwTIf0QKV3UAnYK4PU8uiEc4SRh5jX0mwpIwETPpHdhVM4f53RSwS/vXvN1JhGX+Cs7B8qIq3d6AH49O5fAQ==
dependencies:
"@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"
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"
resolved "https://registry.yarnpkg.com/postcss/-/postcss-8.4.38.tgz#b387d533baf2054288e337066d81c6bee9db9e0e"
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"
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"
resolved "https://registry.yarnpkg.com/source-map-js/-/source-map-js-1.2.0.tgz#16b809c162517b5b8c3e7dcd315a2a5c2612b2af"
integrity sha512-itJW8lvSA0TXEphiRoawsCksnlf8SyvmFzIhltqAHluXd88pkCd+cXJVHTDwdCr0IzwptSm035IHQktUu1QUMg==