#54 user accounts - vuex store and localStorage

This commit is contained in:
Avris 2020-10-15 18:50:32 +02:00
parent 7ac9348396
commit 7453a26773
6 changed files with 217 additions and 82 deletions

View File

@ -39,50 +39,106 @@
</template> </template>
<script> <script>
import { mapState } from 'vuex'
export default { export default {
data() { computed: {
const links = []; ...mapState([
links.push({ link: '/', icon: 'home', text: this.$t('home.header'), textLong: this.$t('home.headerLong'), extra: ['all', this.config.template.any.route] }); 'user',
]),
links() {
const links = [];
if (this.config.sources.enabled) { links.push({
links.push({ link: '/' + this.config.sources.route, icon: 'books', text: this.$t('sources.header'), textLong: this.$t('sources.headerLong') }); link: '/',
} icon: 'home',
text: this.$t('home.header'),
textLong: this.$t('home.headerLong'),
extra: ['all', this.config.template.any.route],
});
if (this.config.nouns.enabled) { if (this.config.sources.enabled) {
links.push({ link: '/' + this.config.nouns.route, icon: 'atom-alt', text: this.$t('nouns.header'), textLong: this.$t('nouns.headerLong') }); links.push({
} link: '/' + this.config.sources.route,
icon: 'books',
text: this.$t('sources.header'),
textLong: this.$t('sources.headerLong'),
});
}
if (this.config.names.enabled) { if (this.config.nouns.enabled) {
links.push({ link: '/' + this.config.names.route, icon: 'signature', text: this.$t('names.header'), textLong: this.$t('names.headerLong') }); links.push({
} link: '/' + this.config.nouns.route,
icon: 'atom-alt',
text: this.$t('nouns.header'),
textLong: this.$t('nouns.headerLong'),
});
}
if (this.config.faq.enabled) { if (this.config.names.enabled) {
links.push({ link: '/' + this.config.faq.route, icon: 'map-marker-question', text: this.$t('faq.header'), textLong: this.$t('faq.headerLong') }); links.push({
} link: '/' + this.config.names.route,
icon: 'signature',
text: this.$t('names.header'),
textLong: this.$t('names.headerLong'),
});
}
if (this.config.links.enabled) { if (this.config.faq.enabled) {
links.push({ link: '/' + this.config.links.route, icon: 'bookmark', text: this.$t('links.header'), textLong: this.$t('links.headerLong') }); links.push({
} link: '/' + this.config.faq.route,
icon: 'map-marker-question',
text: this.$t('faq.header'),
textLong: this.$t('faq.headerLong'),
});
}
if (this.config.people.enabled) { if (this.config.links.enabled) {
links.push({ link: '/' + this.config.people.route, icon: 'user-friends', text: this.$t('people.header'), textLong: this.$t('people.headerLong') }); links.push({
} link: '/' + this.config.links.route,
icon: 'bookmark',
text: this.$t('links.header'),
textLong: this.$t('links.headerLong'),
});
}
if (this.config.english.enabled) { if (this.config.people.enabled) {
links.push({ link: '/' + this.config.english.route, icon: 'globe-americas', text: this.$t('english.header'), textLong: this.$t('english.headerLong') }); links.push({
} link: '/' + this.config.people.route,
icon: 'user-friends',
text: this.$t('people.header'),
textLong: this.$t('people.headerLong'),
});
}
if (this.config.contact.enabled) { if (this.config.english.enabled) {
links.push({ link: '/' + this.config.contact.route, icon: 'comment-alt-smile', text: this.$t('contact.header')}); links.push({
} link: '/' + this.config.english.route,
icon: 'globe-americas',
text: this.$t('english.header'),
textLong: this.$t('english.headerLong'),
});
}
if (this.config.user.enabled) { if (this.config.contact.enabled) {
links.push({ link: '/' + this.config.user.route, icon: 'user', text: this.$t('user.header'), textLong: this.$t('user.headerLong')}); links.push({
} link: '/' + this.config.contact.route,
icon: 'comment-alt-smile',
text: this.$t('contact.header'),
});
}
return { if (this.config.user.enabled) {
links, links.push({
}; link: '/' + this.config.user.route,
icon: 'user',
text: this.user ? '@' + this.user.username : this.$t('user.header'),
textLong: this.user ? '@' + this.user.username : this.$t('user.headerLong'),
});
}
return links;
},
}, },
methods: { methods: {
isActiveRoute(link) { isActiveRoute(link) {

View File

@ -44,6 +44,7 @@ export default {
plugins: [ plugins: [
{ src: '~/plugins/vue-matomo.js', ssr: false }, { src: '~/plugins/vue-matomo.js', ssr: false },
{ src: '~/plugins/globals.js' }, { src: '~/plugins/globals.js' },
{ src: '~/plugins/auth.js' },
], ],
components: true, components: true,
buildModules: [], buildModules: [],

10
plugins/auth.js Normal file
View File

@ -0,0 +1,10 @@
import { Session } from "../src/helpers";
export default ({store}) => {
if (Session.isAvailable()) {
const token = Session.get('token');
if (token) {
store.commit('setToken', token);
}
}
}

View File

@ -5,58 +5,60 @@
<T>user.headerLong</T> <T>user.headerLong</T>
</h2> </h2>
<div v-if="error" class="alert alert-danger"> <section>
<p class="mb-0"> <div v-if="error" class="alert alert-danger">
<Icon v="exclamation-triangle"/>
<T>{{error}}</T>
</p>
</div>
<div v-if="payload && payload.authenticated">
Logged in as <strong>{{payload.uid}}</strong>.
</div>
<div v-else-if="token === null">
<form @submit.prevent="login">
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="usernameOrEmail"
:placeholder="$t('user.login.placeholder')" autofocus required/>
<div class="input-group-append">
<button class="btn btn-primary">
<Icon v="sign-in"/>
<T>user.login.action</T>
</button>
</div>
</div>
</form>
</div>
<div v-else-if="payload && !payload.code">
<div class="alert alert-success">
<p class="mb-0"> <p class="mb-0">
<Icon v="envelope-open-text"/> <Icon v="exclamation-triangle"/>
<T :params="{email: payload.email}">user.login.emailSent</T> <T>{{error}}</T>
</p> </p>
</div> </div>
<form @submit.prevent="validate"> <div v-if="$store.state.user">
<div class="input-group mb-3"> Logged in as <strong>@{{$store.state.user.username}}</strong>.
<input type="text" class="form-control text-center" v-model="code"
placeholder="000000" autofocus required minlength="0" maxlength="6"
inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code"
/>
<div class="input-group-append">
<button class="btn btn-primary">
<Icon v="key"/>
<T>user.code.action</T>
</button>
</div>
</div>
</form>
</div>
<div v-if="token"> <button class="btn btn-outline-secondary btn-sm" @click="logout">
<pre><code>{{JSON.stringify(token)}}</code></pre> <Icon v="sign-out"/>
<pre>{{JSON.stringify(payload, null, 4)}}</pre> Log out
</div> </button>
</div>
<div v-else-if="token === null">
<form @submit.prevent="login">
<div class="input-group mb-3">
<input type="text" class="form-control" v-model="usernameOrEmail"
:placeholder="$t('user.login.placeholder')" autofocus required/>
<div class="input-group-append">
<button class="btn btn-primary">
<Icon v="sign-in"/>
<T>user.login.action</T>
</button>
</div>
</div>
</form>
</div>
<div v-else-if="payload && !payload.code">
<div class="alert alert-success">
<p class="mb-0">
<Icon v="envelope-open-text"/>
<T :params="{email: payload.email}">user.login.emailSent</T>
</p>
</div>
<form @submit.prevent="validate">
<div class="input-group mb-3">
<input type="text" class="form-control text-center" v-model="code"
placeholder="000000" autofocus required minlength="0" maxlength="6"
inputmode="numeric" pattern="[0-9]{6}" autocomplete="one-time-code"
/>
<div class="input-group-append">
<button class="btn btn-primary">
<Icon v="key"/>
<T>user.code.action</T>
</button>
</div>
</div>
</form>
</div>
</section>
</div> </div>
</template> </template>
@ -80,6 +82,8 @@
return null; return null;
} }
this.$store.commit('setToken', this.token);
return jwt.verify(this.token, process.env.PUBLIC_KEY, { return jwt.verify(this.token, process.env.PUBLIC_KEY, {
algorithm: 'RS256', algorithm: 'RS256',
audience: process.env.BASE_URL, audience: process.env.BASE_URL,
@ -107,14 +111,19 @@
const response = await this.$axios.$post(url, data, options); const response = await this.$axios.$post(url, data, options);
this.usernameOrEmail = '';
this.code = '';
if (response.error) { if (response.error) {
this.error = response.error; this.error = response.error;
this.usernameOrEmail = '';
this.code = '';
return; return;
} }
this.token = response.token; this.token = response.token;
},
logout() {
this.token = null;
this.$store.commit('setToken', null);
} }
}, },
head() { head() {

View File

@ -72,3 +72,27 @@ export const makeId = (length, characters = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghi
return result; return result;
} }
export class Session {
static isAvailable() {
return typeof localStorage !== 'undefined';
}
static set(key, value) {
localStorage.setItem(key, JSON.stringify(value));
}
static get(key) {
const value = localStorage.getItem(key);
return key === null ? null : JSON.parse(value);
}
static has(key) {
return localStorage.getItem(key) !== null;
}
static remove(key) {
localStorage.removeItem(key);
}
}

35
store/index.js Normal file
View File

@ -0,0 +1,35 @@
import jwt from 'jsonwebtoken';
import { Session } from '../src/helpers';
export const state = () => ({
token: null,
user: null,
})
export const mutations = {
setToken(state, token) {
if (!token) {
state.token = null;
state.user = null;
Session.remove('token');
return;
}
const user = jwt.verify(token, process.env.PUBLIC_KEY, {
algorithm: 'RS256',
audience: process.env.BASE_URL,
issuer: process.env.BASE_URL,
});
if (user && user.authenticated) {
state.token = token;
state.user = user;
Session.set('token', token);
return;
}
state.token = null;
state.user = null;
Session.remove('token');
}
}