#54 user accounts - vuex store and localStorage
This commit is contained in:
parent
7ac9348396
commit
7453a26773
|
@ -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) {
|
||||||
|
|
|
@ -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: [],
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
107
routes/user.vue
107
routes/user.vue
|
@ -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() {
|
||||||
|
|
|
@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
@ -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');
|
||||||
|
}
|
||||||
|
}
|
Reference in New Issue