mirror of https://github.com/Lissy93/dashy.git
Merge pull request #32 from Lissy93/feature_authentication
[FEATURE] Adds Authentication
This commit is contained in:
commit
6457fdfe24
22
README.md
22
README.md
|
@ -21,8 +21,9 @@
|
|||
- Many options for icons, including full Font-Awesome support and the ability to auto-fetch icon from URLs favicon
|
||||
- Additional info for each item visible on hover (including opening method icon and description as a tooltip)
|
||||
- Option for full-screen background image, custom nav-bar links, and custom footer text
|
||||
- User settings stored in local storage and applied on load
|
||||
- User preferences stored in local storage and applied on load
|
||||
- Encrypted cloud backup and restore feature available
|
||||
- Optional authentication, requiring user to log in
|
||||
- Easy single-file YAML-based configuration
|
||||
- Small bundle size, fully responsive UI and PWA makes the app easy to use on any device
|
||||
- Plus lots more...
|
||||
|
@ -133,6 +134,24 @@ All data is encrypted before being sent to the backend. In Dashy, this is done i
|
|||
|
||||
---
|
||||
|
||||
## Authentication 💂
|
||||
|
||||
> For full development documentation, see: [**Authentication**](./docs/authentication.md)
|
||||
|
||||
Dashy has a built-in login feature, which can be used for basic access control. To enable this feature, add an `auth` attribute under `appConfig`, containing an array of users, each with a username, SHA-256 hashed password and optional user type.
|
||||
|
||||
```yaml
|
||||
appConfig:
|
||||
auth:
|
||||
- user: alicia
|
||||
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
|
||||
```
|
||||
At present, access control is handles on the frontend, and therefore in security-critical applications, it is recommended to use VPN access for authentication.
|
||||
|
||||
**[⬆️ Back to Top](#dashy)**
|
||||
|
||||
---
|
||||
|
||||
## Developing 🧱
|
||||
|
||||
> For full development documentation, see: [**Developing**](./docs/developing.md)
|
||||
|
@ -199,6 +218,7 @@ For more general questions about any of the technologies used, [StackOverflow](h
|
|||
- [Troubleshooting](/docs/troubleshooting.md)
|
||||
- [Backup & Restore](/docs/backup-restore.md)
|
||||
- [Theming](/docs/theming.md)
|
||||
- [Authentication](/docs/authentication.md)
|
||||
|
||||
**[⬆️ Back to Top](#dashy)**
|
||||
|
||||
|
|
|
@ -0,0 +1,160 @@
|
|||
# Authentication
|
||||
|
||||
- [Built-In Login Feature](#authentication)
|
||||
- [Setting Up Authentication](#setting-up-authentication)
|
||||
- [Hash Password](#hash-password)
|
||||
- [Logging In and Out](#logging-in-and-out)
|
||||
- [Security](#security)
|
||||
- [Alternative Authentication Methods](#alternative-authentication-methods)
|
||||
- [VPN](#vpn)
|
||||
- [IP-Based Access](#ip-based-access)
|
||||
- [Web Server Authentication](#web-server-authentication)
|
||||
- [OAuth Services](#oauth-services)
|
||||
- [Auth on Cloud Hosting Services](#static-site-hosting-providers)
|
||||
|
||||
Dashy has a basic login page included, and frontend authentication. You can enable this by adding users to the `auth` section under `appConfig` in your `conf.yml`. If this section is not specified, then no authentication will be required to access the app, and it the homepage will resolve to your dashboard.
|
||||
|
||||
## Setting Up Authentication
|
||||
The `auth` property takes an array of users. Each user needs to include a username, hash and optional user type (`admin` or `normal`). The hash property is a [SHA-256 Hash](https://en.wikipedia.org/wiki/SHA-2) of your desired password.
|
||||
|
||||
For example:
|
||||
```yaml
|
||||
appConfig:
|
||||
auth:
|
||||
- user: alicia
|
||||
hash: 4D1E58C90B3B94BCAD9848ECCACD6D2A8C9FBC5CA913304BBA5CDEAB36FEEFA3
|
||||
type: admin
|
||||
- user: edward
|
||||
hash: 5E884898DA28047151D0E56F8DC6292773603D0D6AABBDD62A11EF721D1542D8
|
||||
type: admin
|
||||
```
|
||||
## Hash Password
|
||||
Dashy uses [SHA-256 Hash](https://en.wikipedia.org/wiki/Sha-256), a 64-character string, which you can generate using an online tool, such as [this one](https://passwordsgenerator.net/sha256-hash-generator/) or [CyberChef](https://gchq.github.io/CyberChef/) (which can be self-hosted/ ran locally).
|
||||
|
||||
A hash is a one-way cryptographic function, meaning that it is easy to generate a hash for a given password, but very hard to determine the original password for a given hash. This means, that so long as your password is long, strong and unique, it is safe to store it's hash in the clear. Having said that, you should never reuse passwords, hashes can be cracked by iterating over known password lists, generating a hash of each.
|
||||
|
||||
## Logging In and Out
|
||||
Once authentication is enabled, so long as there is no valid token in cookie storage, the application will redirect the user to the login page. When the user enters credentials in the login page, they will be checked, and if valid, then a token will be generated, and they can be redirected to the home page. If credentials are invalid, then an error message will be shown, and they will remain on the login page. Once in the application, to log out the user can click the logout button (in the top-right), which will clear cookie storage, causing them to be redirected back to the login page.
|
||||
|
||||
## Security
|
||||
Since all authentication is happening entirely on the client-side, it is vulnerable to manipulation by an adversary. An attacker could look at the source code, find the function used generate the auth token, then decode the minified JavaScript to find the hash, and manually generate a token using it, then just insert that value as a cookie using the console, and become a logged in user. Therefore, if you need secure authentication for your app, it is strongly recommended to implement this using your web server, or use a VPN to control access to Dashy. The purpose of the login page is merely to prevent immediate unauthorized access to your homepage.
|
||||
|
||||
Addressing this is on the todo list, and there are two potential solutions:
|
||||
1. Encrypt all site data against the users password, so that an attacker can not physically access any data without the correct decryption key
|
||||
2. Use a backend service to handle authentication, and do not return user data from the server until the correct credentials are provided. However, this would require either Dashy to be run using it's Node.js server, or the use of an external service
|
||||
|
||||
**[⬆️ Back to Top](#authentication)**
|
||||
|
||||
---
|
||||
|
||||
## Alternative Authentication Methods
|
||||
|
||||
If you are hosting Dashy locally, and require remote access, it is recommend to configure a VPN connection into your local network. For instances running on the cloud, you have several other options:
|
||||
- IP-Based Access
|
||||
- Web Server Authentication
|
||||
- OAuth Services
|
||||
- Password Protection (for cloud providers)
|
||||
|
||||
### VPN
|
||||
The most secure method for accessing Dashy and other self-hosted services remotely is through a VPN connection, using something like [OpenVPN](https://openvpn.net/) or [WireGuard](https://www.wireguard.com/)
|
||||
|
||||
### IP-Based Access
|
||||
If you have a static IP or use a VPN to access your running services, then you can use conditional access to block access to Dashy from everyone except users of your pre-defined IP address. This feature is offered by most cloud providers, and supported by most web servers.
|
||||
|
||||
##### Apache
|
||||
In Apache, this is configured in your `.htaccess` file in Dashy's root folder, and should look something like:
|
||||
```
|
||||
Order Deny,Allow
|
||||
Deny from all
|
||||
Allow from [your-ip]
|
||||
```
|
||||
|
||||
##### NGINX
|
||||
In NGINX you can specify [control access](https://docs.nginx.com/nginx/admin-guide/security-controls/controlling-access-proxied-http/) rules for a given site in your `nginx.conf` or hosts file. For example:
|
||||
```
|
||||
server {
|
||||
listen 80;
|
||||
server_name www.dashy.example.com;
|
||||
location / {
|
||||
root /path/to/dashy/;
|
||||
passenger_enabled on;
|
||||
allow [your-ip];
|
||||
deny all;
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
##### Caddy
|
||||
In Caddy, [Request Matchers](https://caddyserver.com/docs/caddyfile/matchers) can be used to filter requests
|
||||
```
|
||||
dashy.site {
|
||||
@public_networks not remote_ip [your-ip]
|
||||
respond @public_networks "Access denied" 403
|
||||
}
|
||||
```
|
||||
|
||||
### Web Server Authentication
|
||||
Most web servers make password protecting certain apps very easy. Note that you should also set up HTTPS and have a valid certificate in order for this to be secure.
|
||||
|
||||
##### Apache
|
||||
First crate a `.htaccess` file in Dashy's route directory. Specify the auth type and path to where you want to store the password file (usually the same folder). For example:
|
||||
```
|
||||
AuthType Basic
|
||||
AuthName "Please Sign into Dashy"
|
||||
AuthUserFile /path/dashy/.htpasswd
|
||||
require valid-user
|
||||
```
|
||||
|
||||
Then create a `.htpasswd` file in the same directory. List users and their hashed passwords here, with one user on each line, and a colon between username and password (e.g. `[username]:[hashed-password]`). You will need to generate an MD5 hash of your desired password, this can be done with an [online tool](https://www.web2generators.com/apache-tools/htpasswd-generator). Your file will look something like:
|
||||
```
|
||||
alicia:$apr1$jv0spemw$RzOX5/GgY69JMkgV6u16l0
|
||||
```
|
||||
|
||||
##### NGINX
|
||||
NGINX has an [authentication module](https://nginx.org/en/docs/http/ngx_http_auth_basic_module.html) which can be used to add passwords to given sites, and is fairly simple to set up. Similar to above, you will need to create a `.htpasswd` file. Then just enable auth and specify the path to that file, for example:
|
||||
```
|
||||
location / {
|
||||
auth_basic "closed site";
|
||||
auth_basic_user_file conf/htpasswd;
|
||||
}
|
||||
```
|
||||
##### Caddy
|
||||
Caddy has a [basic-auth](https://caddyserver.com/docs/caddyfile/directives/basicauth) directive, where you specify a username and hash. The password hash needs to be base-64 encoded, the [`caddy hash-password`](https://caddyserver.com/docs/command-line#caddy-hash-password) command can help with this. For example:
|
||||
```
|
||||
basicauth /secret/* {
|
||||
alicia JDJhJDEwJEVCNmdaNEg2Ti5iejRMYkF3MFZhZ3VtV3E1SzBWZEZ5Q3VWc0tzOEJwZE9TaFlZdEVkZDhX
|
||||
}
|
||||
```
|
||||
|
||||
##### Lighttpd
|
||||
You can use the [mod_auth](https://doc.lighttpd.net/lighttpd2/mod_auth.html) module to secure your site with Lighttpd. Like with Apache, you need to first create a password file listing your usersnames and hashed passwords, but in Lighttpd, it's usually called `.lighttpdpassword`.
|
||||
|
||||
Then in your `lighttpd.conf` file (usually in the `/etc/lighttpd/` directory), load in the mod_auth module, and configure it's directives. For example:
|
||||
```
|
||||
server.modules += ( "mod_auth" )
|
||||
auth.debug = 2
|
||||
auth.backend = "plain"
|
||||
auth.backend.plain.userfile = "/home/lighttpd/.lighttpdpassword"
|
||||
|
||||
$HTTP["host"] == "dashy.my-domain.net" {
|
||||
server.document-root = "/home/lighttpd/dashy.my-domain.net/http"
|
||||
server.errorlog = "/var/log/lighttpd/dashy.my-domain.net/error.log"
|
||||
accesslog.filename = "/var/log/lighttpd/dashy.my-domain.net/access.log"
|
||||
auth.require = (
|
||||
"/docs/" => (
|
||||
"method" => "basic",
|
||||
"realm" => "Password protected area",
|
||||
"require" => "user=alicia"
|
||||
)
|
||||
)
|
||||
}
|
||||
```
|
||||
Restart your web server for changes to take effect.
|
||||
|
||||
### OAuth Services
|
||||
There are also authentication services, such as [Ory.sh](https://www.ory.sh/), [Okta](https://developer.okta.com/), [Auth0](https://auth0.com/), [Firebase](https://firebase.google.com/docs/auth/). Implementing one of these solutions would involve some changes to the [`Auth.js`](https://github.com/Lissy93/dashy/blob/master/src/utils/Auth.js) file, but should be fairly straight forward.
|
||||
|
||||
## Static Site Hosting Providers
|
||||
If you are hosting Dashy on a cloud platform, you will probably find that it has built-in support for password protected access to web apps. For more info, see the relevant docs for your provider, for example: [Netlify Password Protection](https://docs.netlify.com/visitor-access/password-protection/), [Cloudflare Access](https://www.cloudflare.com/teams/access/), [AWS Cognito](https://aws.amazon.com/cognito/), [Azure Authentication](https://docs.microsoft.com/en-us/azure/app-service/scenario-secure-app-authentication-app-service) and [Vercel Password Protection](https://vercel.com/docs/platform/projects#password-protection).
|
||||
|
||||
**[⬆️ Back to Top](#authentication)**
|
|
@ -55,6 +55,17 @@ All fields are optional, unless otherwise stated.
|
|||
**`externalStyleSheet`** | `string` or `string[]` | _Optional_ | Either a URL to an external stylesheet or an array or URLs, which can be applied as themes within the UI
|
||||
**`customCss`** | `string` | _Optional_ | Raw CSS that will be applied to the page. This can also be set from the UI. Please minify it first.
|
||||
**`showSplashScreen`** | `boolean` | _Optional_ | Should display a splash screen while the app is loading. Defaults to false, except on first load
|
||||
**`auth`** | `array` | _Optional_ | An array of objects containing usernames and hashed passwords. If this is not provided, then authentication will be off by default, and you will not need any credentials to access the app. Note authentication is done on the client side, and so if your instance of Dashy is exposed to the internet, it is recommend to configure your web server to handle this. See [`auth`](#appconfigauth-optional)
|
||||
|
||||
**[⬆️ Back to Top](#configuring)**
|
||||
|
||||
#### `appConfig.auth` _(optional)_
|
||||
|
||||
**Field** | **Type** | **Required**| **Description**
|
||||
--- | --- | --- | ---
|
||||
**`user`** | `string` | Required | Username to log in with
|
||||
**`hash`** | `string` | Required | A SHA-256 hashed password
|
||||
**`type`** | `string` | _Optional_ | The user type, either admin or normal
|
||||
|
||||
**[⬆️ Back to Top](#configuring)**
|
||||
|
||||
|
|
|
@ -299,6 +299,13 @@ Then restart Apache, with `sudo systemctl restart apache2`
|
|||
8. If you need to change the port, click 'Add environmental variable', give it the name 'PORT', choose a port number and press 'Save'.
|
||||
9. Dashy should now be running at your selected path an on a given port
|
||||
|
||||
**[⬆️ Back to Top](#getting-started)**
|
||||
|
||||
---
|
||||
|
||||
## Authentication
|
||||
|
||||
Dashy has built-in client-side authentication, but for security-critical situations, it is recommend to either use a VPN for access, or implement your own authentication using your cloud provider, web server or firewall rules. For more info, see **[Authentication Docs](/docs/authentication.md)**.
|
||||
|
||||
|
||||
**[⬆️ Back to Top](#getting-started)**
|
||||
|
|
|
@ -1,5 +1,3 @@
|
|||
|
||||
|
||||
## Contents
|
||||
|
||||
- [Getting Started](/docs/getting-started.md)
|
||||
|
@ -10,3 +8,4 @@
|
|||
- [Troubleshooting](/docs/troubleshooting.md)
|
||||
- [Backup & Restore](/docs/backup-restore.md)
|
||||
- [Theming](/docs/theming.md)
|
||||
- [Authentication](/docs/authentication.md)
|
||||
|
|
|
@ -0,0 +1 @@
|
|||
<svg aria-hidden="true" focusable="false" data-prefix="fas" data-icon="sign-out-alt" class="svg-inline--fa fa-sign-out-alt fa-w-16" role="img" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 512 512"><path fill="currentColor" d="M497 273L329 441c-15 15-41 4.5-41-17v-96H152c-13.3 0-24-10.7-24-24v-96c0-13.3 10.7-24 24-24h136V88c0-21.4 25.9-32 41-17l168 168c9.3 9.4 9.3 24.6 0 34zM192 436v-40c0-6.6-5.4-12-12-12H96c-17.7 0-32-14.3-32-32V160c0-17.7 14.3-32 32-32h84c6.6 0 12-5.4 12-12V76c0-6.6-5.4-12-12-12H96c-53 0-96 43-96 96v192c0 53 43 96 96 96h84c6.6 0 12-5.4 12-12z"></path></svg>
|
After Width: | Height: | Size: 584 B |
|
@ -1,5 +1,6 @@
|
|||
<template>
|
||||
<button @click="click()">
|
||||
<slot></slot>
|
||||
<slot name="text"></slot>
|
||||
<slot name="icon"></slot>
|
||||
</button>
|
||||
|
|
|
@ -13,6 +13,12 @@
|
|||
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
|
||||
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
|
||||
@modalChanged="modalChanged" />
|
||||
<IconLogout
|
||||
v-if="isUserLoggedIn()"
|
||||
@click="logout()"
|
||||
v-tooltip="'Logout'"
|
||||
class="logout-icon"
|
||||
/>
|
||||
</div>
|
||||
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
|
||||
<button @click="toggleSettingsVisibility()"
|
||||
|
@ -34,6 +40,8 @@ import ThemeSelector from '@/components/Settings/ThemeSelector';
|
|||
import LayoutSelector from '@/components/Settings/LayoutSelector';
|
||||
import ItemSizeSelector from '@/components/Settings/ItemSizeSelector';
|
||||
import KeyboardShortcutInfo from '@/components/Settings/KeyboardShortcutInfo';
|
||||
import { logout as registerLogout } from '@/utils/Auth';
|
||||
import IconLogout from '@/assets/interface-icons/user-logout.svg';
|
||||
import IconOpen from '@/assets/interface-icons/config-open-settings.svg';
|
||||
import IconClose from '@/assets/interface-icons/config-close.svg';
|
||||
|
||||
|
@ -55,6 +63,7 @@ export default {
|
|||
LayoutSelector,
|
||||
ItemSizeSelector,
|
||||
KeyboardShortcutInfo,
|
||||
IconLogout,
|
||||
IconOpen,
|
||||
IconClose,
|
||||
},
|
||||
|
@ -77,6 +86,16 @@ export default {
|
|||
getInitialTheme() {
|
||||
return this.appConfig.theme || '';
|
||||
},
|
||||
logout() {
|
||||
registerLogout();
|
||||
this.$toasted.show('Logged Out');
|
||||
setTimeout(() => {
|
||||
location.reload(); // eslint-disable-line no-restricted-globals
|
||||
}, 100);
|
||||
},
|
||||
isUserLoggedIn() {
|
||||
return !!localStorage[localStorageKeys.USERNAME];
|
||||
},
|
||||
/* Gets user themes if available */
|
||||
getUserThemes() {
|
||||
const userThemes = this.appConfig.cssThemes || [];
|
||||
|
@ -177,6 +196,25 @@ export default {
|
|||
}
|
||||
}
|
||||
|
||||
svg.logout-icon {
|
||||
path {
|
||||
fill: var(--settings-text-color);
|
||||
}
|
||||
width: 1rem;
|
||||
height: 1rem;
|
||||
margin: 0.35rem 0.2rem;
|
||||
padding: 0.2rem;
|
||||
text-align: center;
|
||||
background: var(--background);
|
||||
border: 1px solid var(--settings-text-color);;
|
||||
border-radius: var(--curve-factor);
|
||||
cursor: pointer;
|
||||
&:hover, &.selected {
|
||||
background: var(--settings-text-color);
|
||||
path { fill: var(--background); }
|
||||
}
|
||||
}
|
||||
|
||||
@include tablet {
|
||||
section {
|
||||
display: block;
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
import Vue from 'vue';
|
||||
import Router from 'vue-router';
|
||||
import Home from './views/Home.vue';
|
||||
import Login from './views/Login.vue';
|
||||
import conf from '../public/conf.yml'; // Main site configuration
|
||||
import { pageInfo as defaultPageInfo, localStorageKeys } from './utils/defaults';
|
||||
import { isLoggedIn } from './utils/Auth';
|
||||
|
||||
Vue.use(Router);
|
||||
|
||||
|
@ -21,17 +23,24 @@ try {
|
|||
localAppConfig = undefined;
|
||||
}
|
||||
|
||||
const config = {
|
||||
sections: sections || [],
|
||||
pageInfo: localPageInfo || pageInfo || defaultPageInfo,
|
||||
appConfig: localAppConfig || appConfig || {},
|
||||
};
|
||||
|
||||
const isAuthenticated = () => {
|
||||
const users = config.appConfig.auth;
|
||||
return (!users || isLoggedIn(users));
|
||||
};
|
||||
|
||||
const router = new Router({
|
||||
routes: [
|
||||
{
|
||||
path: '/',
|
||||
name: 'home',
|
||||
component: Home,
|
||||
props: {
|
||||
sections: sections || [],
|
||||
pageInfo: localPageInfo || pageInfo || defaultPageInfo,
|
||||
appConfig: localAppConfig || appConfig || {},
|
||||
},
|
||||
props: config,
|
||||
meta: {
|
||||
title: pageInfo.title || 'Home Page',
|
||||
metaTags: [
|
||||
|
@ -42,6 +51,18 @@ const router = new Router({
|
|||
],
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/login',
|
||||
name: 'login',
|
||||
component: Login,
|
||||
props: {
|
||||
appConfig: config.appConfig,
|
||||
},
|
||||
beforeEnter: (to, from, next) => {
|
||||
if (isAuthenticated()) router.push({ path: '/' });
|
||||
next();
|
||||
},
|
||||
},
|
||||
{
|
||||
path: '/about',
|
||||
name: 'about',
|
||||
|
@ -50,7 +71,12 @@ const router = new Router({
|
|||
],
|
||||
});
|
||||
|
||||
const defaultTitle = 'Speed Dial';
|
||||
router.beforeEach((to, from, next) => {
|
||||
if (to.name !== 'login' && !isAuthenticated()) next({ name: 'login' });
|
||||
else next();
|
||||
});
|
||||
|
||||
const defaultTitle = 'Dashy';
|
||||
router.afterEach((to) => {
|
||||
Vue.nextTick(() => {
|
||||
document.title = to.meta.title || defaultTitle;
|
||||
|
|
|
@ -79,5 +79,8 @@
|
|||
--scroll-bar-background: var(--background-darker);
|
||||
--loading-screen-color: var(--primary);
|
||||
--loading-screen-background: var(--background);
|
||||
--login-form-color: var(--primary);
|
||||
--login-form-background: var(--background);
|
||||
--login-form-background-secondary: var(--background-darker);
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,52 @@
|
|||
import sha256 from 'crypto-js/sha256';
|
||||
import { cookieKeys, localStorageKeys } from './defaults';
|
||||
|
||||
const generateUserToken = (user) => sha256(user.toString()).toString().toLowerCase();
|
||||
|
||||
export const isLoggedIn = (users) => {
|
||||
const validTokens = users.map((user) => generateUserToken(user));
|
||||
let userAuthenticated = false;
|
||||
document.cookie.split(';').forEach((cookie) => {
|
||||
if (cookie && cookie.split('=').length > 1) {
|
||||
const cookieKey = cookie.split('=')[0].trim();
|
||||
const cookieValue = cookie.split('=')[1].trim();
|
||||
if (cookieKey === cookieKeys.AUTH_TOKEN) {
|
||||
if (validTokens.includes(cookieValue)) {
|
||||
userAuthenticated = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
return userAuthenticated;
|
||||
};
|
||||
|
||||
export const checkCredentials = (username, pass, users) => {
|
||||
let response;
|
||||
if (!username) {
|
||||
response = { correct: false, msg: 'Missing Username' };
|
||||
} else if (!pass) {
|
||||
response = { correct: false, msg: 'Missing Password' };
|
||||
} else {
|
||||
users.forEach((user) => {
|
||||
if (user.user === username) {
|
||||
if (user.hash.toLowerCase() === sha256(pass).toString().toLowerCase()) {
|
||||
response = { correct: true, msg: 'Logging in...' };
|
||||
} else {
|
||||
response = { correct: false, msg: 'Incorrect Password' };
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
return response || { correct: false, msg: 'User not found' };
|
||||
};
|
||||
|
||||
export const login = (username, pass) => {
|
||||
const userObject = { user: username, hash: sha256(pass).toString().toLowerCase() };
|
||||
document.cookie = `authenticationToken=${generateUserToken(userObject)}; max-age=600`;
|
||||
localStorage.setItem(localStorageKeys.USERNAME, username);
|
||||
};
|
||||
|
||||
export const logout = () => {
|
||||
document.cookie = 'authenticationToken=null';
|
||||
localStorage.removeItem(localStorageKeys.USERNAME);
|
||||
};
|
|
@ -94,6 +94,38 @@
|
|||
"type": "boolean",
|
||||
"default": false,
|
||||
"description": "Display a loading screen when the app is launched"
|
||||
},
|
||||
"auth": {
|
||||
"type": "array",
|
||||
"description": "Usernames and hashed credentials for frontend authentication",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"additionalProperties": false,
|
||||
"required": [
|
||||
"user",
|
||||
"hash"
|
||||
],
|
||||
"properties": {
|
||||
"user": {
|
||||
"type": "string",
|
||||
"description": "The username for a user"
|
||||
},
|
||||
"hash": {
|
||||
"type": "string",
|
||||
"description": "A SHA-256 hashed password for that user",
|
||||
"minLength": 64,
|
||||
"maxLength": 64
|
||||
},
|
||||
"type": {
|
||||
"enum": [
|
||||
"admin",
|
||||
"normal"
|
||||
],
|
||||
"description": "User type, denoting privilege level, either admin or normal",
|
||||
"default": "normal"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
|
|
|
@ -54,6 +54,10 @@ module.exports = {
|
|||
BACKUP_ID: 'backupId',
|
||||
BACKUP_HASH: 'backupHash',
|
||||
HIDE_SETTINGS: 'hideSettings',
|
||||
USERNAME: 'username',
|
||||
},
|
||||
cookieKeys: {
|
||||
AUTH_TOKEN: 'authenticationToken',
|
||||
},
|
||||
modalNames: {
|
||||
CONF_EDITOR: 'CONF_EDITOR',
|
||||
|
|
|
@ -0,0 +1,132 @@
|
|||
<template>
|
||||
<div class="login-page">
|
||||
<form class="login-form">
|
||||
<h2 class="login-title">Dashy</h2>
|
||||
<Input v-model="username" label="Username" class="login-field username" type="text" />
|
||||
<Input v-model="password" label="Password" class="login-field password" type="password" />
|
||||
<Button class="login-button" :click="submitLogin">Login</Button>
|
||||
<transition name="bounce">
|
||||
<p :class="`login-error-message ${status}`" v-show="message">{{ message }}</p>
|
||||
</transition>
|
||||
</form>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import router from '@/router';
|
||||
import Button from '@/components/FormElements/Button';
|
||||
import Input from '@/components/FormElements/Input';
|
||||
import Defaults, { localStorageKeys } from '@/utils/defaults';
|
||||
import { checkCredentials, login } from '@/utils/Auth';
|
||||
|
||||
export default {
|
||||
name: 'login',
|
||||
props: {
|
||||
appConfig: Object,
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
username: '',
|
||||
password: '',
|
||||
message: '',
|
||||
status: 'waiting', // wating, error, success
|
||||
};
|
||||
},
|
||||
components: {
|
||||
Button,
|
||||
Input,
|
||||
},
|
||||
methods: {
|
||||
submitLogin() {
|
||||
const response = checkCredentials(this.username, this.password, this.appConfig.auth || []);
|
||||
this.message = response.msg; // Show error or success message to the user
|
||||
this.status = response.correct ? 'success' : 'error';
|
||||
if (response.correct) { // Yay, credentials were correct :)
|
||||
login(this.username, this.password); // Login, to set the cookie
|
||||
setTimeout(() => { // Wait a short while, then redirect back home
|
||||
router.push({ path: '/' });
|
||||
}, 250);
|
||||
}
|
||||
},
|
||||
setTheme() {
|
||||
const theme = localStorage[localStorageKeys.THEME] || Defaults.theme;
|
||||
document.getElementsByTagName('html')[0].setAttribute('data-theme', theme);
|
||||
},
|
||||
},
|
||||
created() {
|
||||
this.setTheme();
|
||||
},
|
||||
};
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
.login-page {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
min-height: 800px;
|
||||
|
||||
.login-form {
|
||||
background: var(--login-form-background);
|
||||
color: var(--login-form-color);
|
||||
border: 1px solid var(--login-form-color);
|
||||
border-radius: var(--curve-factor);
|
||||
padding: 2rem;
|
||||
margin: 2rem auto;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
|
||||
h2.login-title {
|
||||
font-size: 3rem;
|
||||
margin: 0 0 1rem 0;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.login-field input, Button.login-button {
|
||||
width: 18rem;
|
||||
margin: 0.5rem auto;
|
||||
font-size: 1.4rem;
|
||||
padding: 0.5rem 1rem;
|
||||
}
|
||||
|
||||
.login-field input {
|
||||
color: var(--login-form-color);
|
||||
border-color: var(--login-form-background-secondary);
|
||||
background: var(--login-form-background-secondary);
|
||||
&:focus {
|
||||
|
||||
}
|
||||
}
|
||||
Button.login-button {
|
||||
background: var(--login-form-background-secondary);
|
||||
border-color: var(--login-form-background-secondary);
|
||||
&:hover {
|
||||
border-color: var(--login-form-color);
|
||||
color: var(--login-form-background-secondary);
|
||||
background: var(--login-form-color);
|
||||
}
|
||||
&:active, &:focus {
|
||||
box-shadow: 1px 1px 6px var(--login-form-color);
|
||||
}
|
||||
}
|
||||
p.login-error-message {
|
||||
font-size: 1rem;
|
||||
text-align: center;
|
||||
&.waiting { color: var(--login-form-color); }
|
||||
&.success { color: var(--success); }
|
||||
&.error { color: var(--warning); }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.bounce-enter-active { animation: bounce-in 0.25s; }
|
||||
.bounce-leave-active { animation: bounce-in 0.25s reverse; }
|
||||
@keyframes bounce-in {
|
||||
0% { transform: scale(0); }
|
||||
50% { transform: scale(1.25); }
|
||||
100% { transform: scale(1); }
|
||||
}
|
||||
|
||||
</style>
|
Loading…
Reference in New Issue