mirror of https://github.com/Lissy93/dashy.git
Adds functionality and supporting components for frontend authentication
This commit is contained in:
parent
8665c6010d
commit
25ee90b987
|
@ -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);
|
||||
};
|
|
@ -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