restruct + 2fa + auth

This commit is contained in:
2023-07-11 16:53:17 +02:00
parent d0647ba1bb
commit 865141d945
26 changed files with 279 additions and 127 deletions

View File

@@ -36,6 +36,7 @@ app.use( cookieParser() );
app.use( express.static( '../webapp/dist' ) ); app.use( express.static( '../webapp/dist' ) );
require( './admin/routes.js' )( app, settings ); // admin route require( './admin/routes.js' )( app, settings ); // admin route
require( './backend/userRoutes.js' )( app, settings ); // user route
app.use( ( request, response ) => { app.use( ( request, response ) => {
response.sendFile( path.join( __dirname + '/../webapp/dist/index.html' ) ); response.sendFile( path.join( __dirname + '/../webapp/dist/index.html' ) );

View File

@@ -0,0 +1,25 @@
/*
* libreevent - pwdmanager.js
*
* Created by Janis Hutz 07/11/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
/*
These functions are required to verify user login and to create new users
and to hash new passwords (if user changes password.)
*/
// import and init
const bcrypt = require( 'bcrypt' );
const db = require( '../db/db.js' );
module.exports.checkpassword = function checkpassword ( username, password ) {
return new Promise( resolve => {
db.getData( 'user', username ).then( data => {
resolve( bcrypt.compareSync( password, data ) );
} );
} );
};

View File

@@ -11,7 +11,6 @@ const path = require( 'path' );
const fs = require( 'fs' ); const fs = require( 'fs' );
module.exports.getData = function getData ( db, searchQuery ) { module.exports.getData = function getData ( db, searchQuery ) {
console.log( db + searchQuery );
return new Promise( resolve => { return new Promise( resolve => {
resolve( '$2b$05$ElMYWoMjk7567lXkIkee.e.6cxCrWU4gkfuNLB8gmGYLQQPm7gT3O' ); resolve( '$2b$05$ElMYWoMjk7567lXkIkee.e.6cxCrWU4gkfuNLB8gmGYLQQPm7gT3O' );
} ); } );

View File

@@ -1 +1,14 @@
class /*
* libreevent - jsondb.js
*
* Created by Janis Hutz 07/11/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
class JSONDB {
constructor () {
}
}

View File

@@ -1,17 +0,0 @@
/*
* libreevent - routes.js
*
* Created by Janis Hutz 07/11/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const db = require( './db/db.js' );
module.exports = ( app, settings ) => {
app.post( '/api/reserveTicket', ( request, response ) ) {
db.getData( 'test', request.body );
response.send( 'ok' );
};
};

View File

@@ -0,0 +1,38 @@
/*
* libreevent - routes.js
*
* Created by Janis Hutz 07/11/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
*/
const db = require( './db/db.js' );
const pwdmanager = require( './credentials/pwdmanager.js' );
module.exports = ( app, settings ) => {
app.post( '/api/reserveTicket', ( request, response ) => {
db.getData( 'test', request.body );
response.send( 'ok' );
} );
app.post( '/user/login', ( request, response ) => {
if ( request.body.mail && request.body.password ) {
pwdmanager.checkpassword( request.body.mail, request.body.password ).then( data => {
if ( data ) {
if ( settings.twoFA ) {
// TODO: Support both methods of 2fa
response.send( '2fa' );
} else {
request.session.loggedInUser = true;
response.send( 'ok' );
}
} else {
response.send( 'pwErr' );
}
} );
} else {
response.send( 'missingCredentials' );
}
} );
};

View File

@@ -1,3 +1,4 @@
{ {
"init":true "init":true,
"twoFA": true
} }

View File

@@ -1,6 +1,6 @@
<template> <template>
<div id="notifications" @click="handleNotifications();"> <div id="notifications" @click="handleNotifications();">
<div class="message-box" :class="location"> <div class="message-box" :class="[ location, size ]">
<div class="message-container" :class="messageType"> <div class="message-container" :class="messageType">
<span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span> <span class="material-symbols-outlined types hide" v-if="messageType == 'hide'">question_mark</span>
<span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span> <span class="material-symbols-outlined types" v-else-if="messageType == 'ok'" style="background-color: green;">done</span>
@@ -21,7 +21,12 @@
location: { location: {
type: String, type: String,
'default': 'topleft', 'default': 'topleft',
},
size: {
type: String,
'default': 'default',
} }
// Size options: small, default (default option), big, bigger, huge
}, },
data () { data () {
return { return {
@@ -119,9 +124,32 @@
position: fixed; position: fixed;
z-index: 10; z-index: 10;
color: white; color: white;
transition: all 0.5s;
}
.default {
height: 10vh; height: 10vh;
width: 15vw; width: 15vw;
transition: all 0.5s; }
.small {
height: 7vh;
width: 11vw;
}
.big {
height: 12vh;
width: 17vw;
}
.bigger {
height: 15vh;
width: 20vw;
}
.huge {
height: 20vh;
width: 25vw;
} }
.topleft { .topleft {

View File

@@ -55,6 +55,10 @@ router.beforeEach( ( to, from ) => {
return { name: 'purchase' }; return { name: 'purchase' };
} else if ( to.name.substring( 0, 5 ) === 'setup' && !backendStore.getVisitedSetupPages[ to.name.substring( 5 ).toLowerCase() ] && to.name.substring( 5 ).toLowerCase() !== 'start' && to.name.substring( 5 ).toLowerCase() !== 'root' ) { } else if ( to.name.substring( 0, 5 ) === 'setup' && !backendStore.getVisitedSetupPages[ to.name.substring( 5 ).toLowerCase() ] && to.name.substring( 5 ).toLowerCase() !== 'start' && to.name.substring( 5 ).toLowerCase() !== 'root' ) {
return { name: 'setupStart' }; return { name: 'setupStart' };
} else if ( to.name === '2fa' && !userStore.getUserTwoFACompliant ) {
return { name: 'login' };
} else if ( to.name === 'Admin2fa' && !userStore.getAdminTwoFACompliant ) {
return { name: 'adminLogin' };
} }
} ); } );

View File

@@ -27,7 +27,7 @@ export default [
{ {
path: '/tickets', path: '/tickets',
name: 'tickets', name: 'tickets',
component: () => import( '../views/OrderView.vue' ), component: () => import( '../views/purchasing/OrderView.vue' ),
meta: { meta: {
title: 'Order ticket - libreevent' title: 'Order ticket - libreevent'
} }
@@ -35,7 +35,7 @@ export default [
{ {
path: '/login', path: '/login',
name: 'login', name: 'login',
component: () => import( '../views/LoginView.vue' ), component: () => import( '../views/user/LoginView.vue' ),
meta: { meta: {
title: 'Login - libreevent' title: 'Login - libreevent'
} }
@@ -43,7 +43,7 @@ export default [
{ {
path: '/admin/login', path: '/admin/login',
name: 'adminLogin', name: 'adminLogin',
component: () => import( '../views/AdminLoginView.vue' ), component: () => import( '../views/admin/AdminLoginView.vue' ),
meta: { meta: {
title: 'Login :: Admin - libreevent' title: 'Login :: Admin - libreevent'
} }
@@ -51,7 +51,7 @@ export default [
{ {
path: '/signup', path: '/signup',
name: 'signup', name: 'signup',
component: () => import( '../views/SignupView.vue' ), component: () => import( '../views/user/SignupView.vue' ),
meta: { meta: {
title: 'Signup - libreevent' title: 'Signup - libreevent'
} }
@@ -59,15 +59,23 @@ export default [
{ {
path: '/account', path: '/account',
name: 'account', name: 'account',
component: () => import( '../views/AccountView.vue' ), component: () => import( '../views/user/AccountView.vue' ),
meta: { meta: {
title: 'Account - libreevent' title: 'Account - libreevent'
} }
}, },
{
path: '/twoFactors',
name: '2fa',
component: () => import( '../views/user/TwoFA.vue' ),
meta: {
title: 'Two Factor Authentication - libreevent'
}
},
{ {
path: '/tickets/details', path: '/tickets/details',
name: 'ticketDetails', name: 'ticketDetails',
component: () => import( '../views/TicketsDetailsView.vue' ), component: () => import( '../views/purchasing/TicketsDetailsView.vue' ),
meta: { meta: {
title: 'Details - libreevent', title: 'Details - libreevent',
transition: 'scale' transition: 'scale'
@@ -76,7 +84,7 @@ export default [
{ {
path: '/tickets/order', path: '/tickets/order',
name: 'ticketOrder', name: 'ticketOrder',
component: () => import( '../views/TicketsOrderingView.vue' ), component: () => import( '../views/purchasing/TicketsOrderingView.vue' ),
meta: { meta: {
title: 'Order ticket - libreevent', title: 'Order ticket - libreevent',
transition: 'scale' transition: 'scale'
@@ -85,7 +93,7 @@ export default [
{ {
path: '/cart', path: '/cart',
name: 'cart', name: 'cart',
component: () => import( '../views/CartView.vue' ), component: () => import( '../views/purchasing/CartView.vue' ),
meta: { meta: {
title: 'Cart - libreevent', title: 'Cart - libreevent',
transition: 'scale' transition: 'scale'
@@ -94,7 +102,7 @@ export default [
{ {
path: '/purchase', path: '/purchase',
name: 'purchase', name: 'purchase',
component: () => import( '@/views/PurchaseView.vue' ), component: () => import( '@/views/purchasing/PurchaseView.vue' ),
meta: { meta: {
title: 'Purchase - libreevent', title: 'Purchase - libreevent',
transition: 'scale' transition: 'scale'
@@ -103,21 +111,12 @@ export default [
{ {
path: '/pay', path: '/pay',
name: 'pay', name: 'pay',
component: () => import( '@/views/PaymentView.vue' ), component: () => import( '@/views/purchasing/PaymentView.vue' ),
meta: { meta: {
title: 'Pay - libreevent', title: 'Pay - libreevent',
transition: 'scale', transition: 'scale',
} }
}, },
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import( '@/views/404.vue' ),
meta: {
title: '404 - Page not found :: libreevent',
transition: 'scale',
}
},
{ {
path: '/admin/seatplan', path: '/admin/seatplan',
name: 'adminSeatplanEditor', name: 'adminSeatplanEditor',
@@ -136,4 +135,13 @@ export default [
adminAuthRequired: true, adminAuthRequired: true,
} }
}, },
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import( '@/views/404.vue' ),
meta: {
title: '404 - Page not found :: libreevent',
transition: 'scale',
}
},
] ]

View File

@@ -10,7 +10,7 @@
export default { export default {
path: '/setup', path: '/setup',
name: 'setup', name: 'setup',
component: () => import( '../views/SetupView.vue' ), component: () => import( '../views/setup/SetupView.vue' ),
meta: { meta: {
title: 'Login :: Admin - libreevent', title: 'Login :: Admin - libreevent',
setupAuthRequired: true, setupAuthRequired: true,

View File

@@ -10,10 +10,12 @@
import { defineStore } from "pinia"; import { defineStore } from "pinia";
export const useUserStore = defineStore ( 'user', { export const useUserStore = defineStore ( 'user', {
state: () => ( { 'isUserAuth': true, 'isAdminAuth': true, 'userData': {} } ), state: () => ( { 'isUserAuth': false, 'isAdminAuth': false, 'userData': {}, 'isTwoFACompliantUser': false, 'isTwoFACompliantAdmin': false } ),
getters: { getters: {
getUserAuthenticated: ( state ) => state.isUserAuth, getUserAuthenticated: ( state ) => state.isUserAuth,
getAdminAuthenticated: ( state ) => state.isAdminAuth, getAdminAuthenticated: ( state ) => state.isAdminAuth,
getUserTwoFACompliant: ( state ) => state.isTwoFACompliantUser,
getAdminTwoFACompliant: ( state ) => state.isTwoFACompliantAdmin,
}, },
actions: { actions: {
setUserAuth ( auth ) { setUserAuth ( auth ) {
@@ -21,6 +23,12 @@ export const useUserStore = defineStore ( 'user', {
}, },
setAdminAuth ( auth ) { setAdminAuth ( auth ) {
this.isAdminAuth = auth; this.isAdminAuth = auth;
},
setUser2fa ( auth ) {
this.isTwoFACompliantUser = auth;
},
setAdmin2fa ( auth ) {
this.isTwoFACompliantAdmin = auth;
} }
} }
} ); } );

View File

@@ -1,84 +0,0 @@
<!--
* libreevent - LoginView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="login">
<div class="login-app">
<h1>Log in</h1>
<form>
<label for="mail">Email</label><br>
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required><br><br>
<label for="password">Password</label><br>
<input type="password" v-model="formData[ 'password' ]" name="password" id="password" required>
</form>
<button @click="login();" class="button">Log in</button>
<router-link to="/signup" class="button">Sign up instead</router-link>
</div>
</div>
</template>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
export default {
data () {
return {
formData: {}
}
},
computed: {
...mapStores( useUserStore )
},
methods: {
login () {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ? sessionStorage.getItem( 'redirect' ) : '/account' );
sessionStorage.removeItem( 'redirect' );
}
},
}
</script>
<style scoped>
/* TODO: Update colour to image */
.login {
background-color: green;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0 1 auto;
}
.login-app {
background-color: var( --background-color );
min-height: fit-content;
min-height: fit-content;
padding: 5% 20%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50px;
}
.button {
padding: 5px 10px;
margin-top: 2%;
}
nav {
display: initial;
}
</style>

View File

@@ -0,0 +1,122 @@
<!--
* libreevent - LoginView.vue
*
* Created by Janis Hutz 05/14/2023, Licensed under the GPL V3 License
* https://janishutz.com, development@janishutz.com
*
*
-->
<template>
<div class="login">
<div class="login-app">
<h1>Log in</h1>
<form>
<label for="mail">Email</label><br>
<input type="email" v-model="formData[ 'mail' ]" name="mail" id="mail" required><br><br>
<label for="password">Password</label><br>
<input type="password" v-model="formData[ 'password' ]" name="password" id="password" required>
</form>
<button @click="login();" class="button">Log in</button>
<router-link to="/signup" class="button">Sign up instead</router-link>
</div>
<notifications ref="notification" location="topright" size="bigger"></notifications>
</div>
</template>
<script>
import { useUserStore } from '@/stores/userStore';
import { mapStores } from 'pinia';
import notifications from '@/components/notifications/notifications.vue';
export default {
data () {
return {
formData: {}
}
},
components: {
notifications,
},
computed: {
...mapStores( useUserStore )
},
methods: {
login () {
if ( this.formData.mail ) {
if ( this.formData.password ) {
let fetchOptions = {
method: 'post',
body: JSON.stringify( this.formData ),
headers: {
'Content-Type': 'application/json',
'charset': 'utf-8'
}
};
fetch( localStorage.getItem( 'url' ) + '/user/login', fetchOptions ).then( res => {
res.text().then( text => {
console.log( text );
if ( text === 'ok' ) {
this.userStore.setUserAuth( true );
this.$router.push( sessionStorage.getItem( 'redirect' ) ? sessionStorage.getItem( 'redirect' ) : '/account' );
sessionStorage.removeItem( 'redirect' );
} else if ( text === '2fa' ) {
this.userStore.setUser2fa( true );
this.$router.push( '/twoFactors' );
} else {
this.$refs.notification.createNotification( 'The credentials you provided do not match our records.', 5, 'error', 'normal' );
}
} );
} );
} else {
this.$refs.notification.createNotification( 'A password is required to log in', 5, 'error', 'normal' );
}
} else {
this.$refs.notification.createNotification( 'An email address is required to log in', 5, 'error', 'normal' );
}
},
},
}
</script>
<style scoped>
/* TODO: Update colour to image */
.login {
background-color: green;
width: 100%;
height: 100%;
flex-grow: 1;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
flex: 0 1 auto;
}
.login-app {
background-color: var( --background-color );
min-height: fit-content;
min-height: fit-content;
padding: 5% 20%;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border-radius: 50px;
}
.button {
padding: 5px 10px;
margin-top: 2%;
}
nav {
display: initial;
}
#missing-email, #missing-password, #credentials-wrong {
display: none;
margin-bottom: 20px;
}
</style>

View File

@@ -0,0 +1,6 @@
<template>
<div id="2fa">
<h1>Two Factor Authentication</h1>
<p>We have sent you an email containing a link for Authentication.</p>
</div>
</template>