mirror of
https://github.com/Lissy93/dashy.git
synced 2025-07-23 13:45:33 +02:00
Adds functionality and supporting components for frontend authentication
This commit is contained in:
parent
8665c6010d
commit
25ee90b987
1
src/assets/interface-icons/user-logout.svg
Normal file
1
src/assets/interface-icons/user-logout.svg
Normal file
@ -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>
|
<template>
|
||||||
<button @click="click()">
|
<button @click="click()">
|
||||||
|
<slot></slot>
|
||||||
<slot name="text"></slot>
|
<slot name="text"></slot>
|
||||||
<slot name="icon"></slot>
|
<slot name="icon"></slot>
|
||||||
</button>
|
</button>
|
||||||
|
@ -13,6 +13,12 @@
|
|||||||
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
|
<ItemSizeSelector :iconSize="iconSize" @iconSizeUpdated="updateIconSize" />
|
||||||
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
|
<ConfigLauncher :sections="sections" :pageInfo="pageInfo" :appConfig="appConfig"
|
||||||
@modalChanged="modalChanged" />
|
@modalChanged="modalChanged" />
|
||||||
|
<IconLogout
|
||||||
|
v-if="isUserLoggedIn()"
|
||||||
|
@click="logout()"
|
||||||
|
v-tooltip="'Logout'"
|
||||||
|
class="logout-icon"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
|
<div :class="`show-hide-container ${settingsVisible? 'hide-btn' : 'show-btn'}`">
|
||||||
<button @click="toggleSettingsVisibility()"
|
<button @click="toggleSettingsVisibility()"
|
||||||
@ -34,6 +40,8 @@ import ThemeSelector from '@/components/Settings/ThemeSelector';
|
|||||||
import LayoutSelector from '@/components/Settings/LayoutSelector';
|
import LayoutSelector from '@/components/Settings/LayoutSelector';
|
||||||
import ItemSizeSelector from '@/components/Settings/ItemSizeSelector';
|
import ItemSizeSelector from '@/components/Settings/ItemSizeSelector';
|
||||||
import KeyboardShortcutInfo from '@/components/Settings/KeyboardShortcutInfo';
|
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 IconOpen from '@/assets/interface-icons/config-open-settings.svg';
|
||||||
import IconClose from '@/assets/interface-icons/config-close.svg';
|
import IconClose from '@/assets/interface-icons/config-close.svg';
|
||||||
|
|
||||||
@ -55,6 +63,7 @@ export default {
|
|||||||
LayoutSelector,
|
LayoutSelector,
|
||||||
ItemSizeSelector,
|
ItemSizeSelector,
|
||||||
KeyboardShortcutInfo,
|
KeyboardShortcutInfo,
|
||||||
|
IconLogout,
|
||||||
IconOpen,
|
IconOpen,
|
||||||
IconClose,
|
IconClose,
|
||||||
},
|
},
|
||||||
@ -77,6 +86,16 @@ export default {
|
|||||||
getInitialTheme() {
|
getInitialTheme() {
|
||||||
return this.appConfig.theme || '';
|
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 */
|
/* Gets user themes if available */
|
||||||
getUserThemes() {
|
getUserThemes() {
|
||||||
const userThemes = this.appConfig.cssThemes || [];
|
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 {
|
@include tablet {
|
||||||
section {
|
section {
|
||||||
display: block;
|
display: block;
|
||||||
|
@ -1,8 +1,10 @@
|
|||||||
import Vue from 'vue';
|
import Vue from 'vue';
|
||||||
import Router from 'vue-router';
|
import Router from 'vue-router';
|
||||||
import Home from './views/Home.vue';
|
import Home from './views/Home.vue';
|
||||||
|
import Login from './views/Login.vue';
|
||||||
import conf from '../public/conf.yml'; // Main site configuration
|
import conf from '../public/conf.yml'; // Main site configuration
|
||||||
import { pageInfo as defaultPageInfo, localStorageKeys } from './utils/defaults';
|
import { pageInfo as defaultPageInfo, localStorageKeys } from './utils/defaults';
|
||||||
|
import { isLoggedIn } from './utils/Auth';
|
||||||
|
|
||||||
Vue.use(Router);
|
Vue.use(Router);
|
||||||
|
|
||||||
@ -21,17 +23,24 @@ try {
|
|||||||
localAppConfig = undefined;
|
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({
|
const router = new Router({
|
||||||
routes: [
|
routes: [
|
||||||
{
|
{
|
||||||
path: '/',
|
path: '/',
|
||||||
name: 'home',
|
name: 'home',
|
||||||
component: Home,
|
component: Home,
|
||||||
props: {
|
props: config,
|
||||||
sections: sections || [],
|
|
||||||
pageInfo: localPageInfo || pageInfo || defaultPageInfo,
|
|
||||||
appConfig: localAppConfig || appConfig || {},
|
|
||||||
},
|
|
||||||
meta: {
|
meta: {
|
||||||
title: pageInfo.title || 'Home Page',
|
title: pageInfo.title || 'Home Page',
|
||||||
metaTags: [
|
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',
|
path: '/about',
|
||||||
name: '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) => {
|
router.afterEach((to) => {
|
||||||
Vue.nextTick(() => {
|
Vue.nextTick(() => {
|
||||||
document.title = to.meta.title || defaultTitle;
|
document.title = to.meta.title || defaultTitle;
|
||||||
|
@ -79,5 +79,8 @@
|
|||||||
--scroll-bar-background: var(--background-darker);
|
--scroll-bar-background: var(--background-darker);
|
||||||
--loading-screen-color: var(--primary);
|
--loading-screen-color: var(--primary);
|
||||||
--loading-screen-background: var(--background);
|
--loading-screen-background: var(--background);
|
||||||
|
--login-form-color: var(--primary);
|
||||||
|
--login-form-background: var(--background);
|
||||||
|
--login-form-background-secondary: var(--background-darker);
|
||||||
|
|
||||||
}
|
}
|
||||||
|
52
src/utils/Auth.js
Normal file
52
src/utils/Auth.js
Normal file
@ -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_ID: 'backupId',
|
||||||
BACKUP_HASH: 'backupHash',
|
BACKUP_HASH: 'backupHash',
|
||||||
HIDE_SETTINGS: 'hideSettings',
|
HIDE_SETTINGS: 'hideSettings',
|
||||||
|
USERNAME: 'username',
|
||||||
|
},
|
||||||
|
cookieKeys: {
|
||||||
|
AUTH_TOKEN: 'authenticationToken',
|
||||||
},
|
},
|
||||||
modalNames: {
|
modalNames: {
|
||||||
CONF_EDITOR: 'CONF_EDITOR',
|
CONF_EDITOR: 'CONF_EDITOR',
|
||||||
|
132
src/views/Login.vue
Normal file
132
src/views/Login.vue
Normal file
@ -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…
x
Reference in New Issue
Block a user