Vue.js with Vue-Router AJAX call before application starts - vue.js

I'm creating simple application with allows to login, restore password and some other actions for logged users.
I am also using Vue-Router. I want to create 3 different types of routes:
allow only for logged users (example: change password)
allow only for unlogged users (example: restore password)
allow for everyone (example: homepage)
I have two functions with are calling before all routes
router.beforeEach(Authentication.OnlyLoggedAllowed)
router.beforeEach(Authentication.OnlyNotLoggedAllowed)
I also want to login user if it's possible (AJAX call to API). To use it I have tried to add code BEFORE route.beforeEach(...)
import Vue from "vue"
import VueRouter from "vue-router"
import VueResource from "vue-resource"
...
Authentication.TryLogin.bind(Vue)();
...
router.beforeEach(Authentication.OnlyLoggedAllowed)
router.beforeEach(Authentication.OnlyNotLoggedAllowed)
new Vue({
router
}).$mount("div#application")
Is it stupid to add function TryLogin() to router.beforeEach(..)? AJAX call after every page change is not really smart.
But I have problem because this.$http in my TryLogin() function returns undefined.
Or maybe I should raw AJAX call if there's no other way.
After all I will make this call synchronous.

There is no shame in using jQuery, for example, for ajax calls.
If you have a global store (using vuex here) you can set a boolean to true after the first TryLogin, so you don't call it more than once after further route changes. Something like that:
import Vue from 'vue';
import Router from 'vue-router';
import store from './store';
Vue.use(Router);
const router = new Router({
mode: 'history',
routes: [
{
path: '/',
component: Home,
},
{
path: '/profile',
component: Profile,
meta: {loggedIn: true}
},
{
path: '/signup',
component: Signup,
meta: {loggedOut: true},
}
],
});
router.beforeEach(async (to, from, next) => {
if (!store.state.userLoaded) {
await $.get('/api/account').then(
// Gets the user (or null) from the server, and
// set userLoaded to true in any case
user => store.commit('updateUser', user),
err => console.error(err)
}
}
// Check if the user needs to be logged in
if (to.meta.loggedIn && !store.state.user) {
return next ({path: '/login', query: {redirectTo: to.fullPath}});
// Check if the user needs to be logged out
} else if (to.meta.loggedOut && store.state.user) {
return next ({path: '/'});
}
// We can proceeed
next();
}
export default router;
The store.commit('updateUser', user) will set userLoaded to true regardless of whether the user is actually logged in.
Here is an example of the vuex store used:
import Vue from 'vue';
import Vuex from 'vuex';
Vue.use(Vuex);
export default new Vuex.Store({
state: {
user: null,
userLoaded: false
},
mutations: {
updateUser: (state, user) => {
state.user = user;
state.userLoaded = true;
}
}
});
And the main vue file:
import router from './router';
import store from './store';
new Vue({
router,
store
}).$mount("div#application")

Related

Vuejs - Trying to get the updated store.state data in my router

I’m trying to get the store.state.role data in my router to determine which routes file it should use for the routes.
When i console.log(store.state) i see that the store.state.role has the correct data i need. But when i target the parameter like console.log(store.state.role) it shows the default value from my store which isn't the same as what i see when i console.log(store.state).
this is my store file
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
state:
{
//User
login_state: false,
refreshUser: false,
role: '',
target: '',
currentProject: '',
},
mutations:
{
//Update user token
updateUserToken(state, payload)
{
localStorage.setItem('token', payload);
state.login_state = true;
},
//Update role
updateRole(state, payload)
{
state.role = payload;
},
//Login state switch
loginStateUpdate(state, payload)
{
state.login_state = payload;
},
//Refresh userdata
userRefresh(state)
{
state.refreshUser = !state.refreshUser
},
updateProject(state, payload)
{
state.currentProject = payload
}
},
getters:
{
},
actions:
{
},
modules:
{
}
})
this is my router file
import Vue from "vue";
import VueRouter from "vue-router";
import Tenant from "./tenant";
import Tenancy from "./tenancy";
import {tenancy} from '../resources/api.config'
import {Http} from '#/util/http'
import store from '../store'
Vue.use(VueRouter);
const host = window.location.host.toLowerCase().split(".")[0];
let routes;
if (host != tenancy.toLowerCase())
{
console.log(store.state.role) //Returns '' while it's 'admin'
routes = Tenant;
store.commit('updateTarget', 'tenant');
}
else
{
console.log(store.state.role) //Returns '' while it's 'admin'
routes = Tenancy;
store.commit('updateTarget', 'tenancy');
}
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
export default router;
Why is this happening and how can i get the data i need ?
#neavehni's answer, using Local Storage, may add some unwanted side-effects, such as if multiple people use the the same device. Some better options:
Navigation Guards
Using Navigation Guards, add all routes at once, then implement navigation guards to prevent users from navigating to routes that they don't have permission for. (See documentation on Navigation Guards). You can use router.beforeEach if it is possible to make a unified implementation for all routes, or define the beforeEnter guard on each route if each route has very custom requirements. One option to simplify the navigation guard code is to include some information in the meta object of each route. This object allows you to define and store any data you wish in association with the route, which can be checked in the beforeEach global navigation guard.
Dynamically Load Routes at Login
An alternative is to only load a few shared routes in router.js. Then, after the user logs in, add the appropriate routes using the router's addRoutes method. This method accepts an array of RouteConfig objects, so you could use the same route definitions you currently have. See documentation here.
The only solution i've found is instead of using vuex store you use the localstorage.

Vue Router: Load routes based on user role

I'd like to use the same route path /dashboard to load slightly different views for my users based on their user role.
Previously, I've loaded in different routes based on the current domain:
import MarketingSiteRoutes from './marketingSiteRoutes'
import DashboardRoutes from './dashboardRoutes'
import CampusRoutes from './campusRoutes'
Vue.use(VueRouter)
const host = window.location.host
const parts = host.split('.')
let routes
if (parts.length === 3) {
const subdomain = parts[0]
switch (subdomain) {
case 'dashboard':
routes = DashboardRoutes
break
case 'demo':
routes = CampusRoutes
break
default:
}
} else {
routes = MarketingSiteRoutes
}
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
This approach only relies on subdomain, which is available on creation, so everything can be synchronous. I would have to authenticate the user, load in the correct routes based on their role, and then export the router to initialize the vue app.
Ideally, I could load in ...adminRoutes, ...parentRoutes, ...childRoutes based on the user's role, which could contain all the route definitions for the role.
I'm using Feathers-vuex, but I can't seem to await the user and then export the router. Any way to do this?
I would use a main Dashboard component, that checks the user role, and then shows the correct child component.
<template>
<div>
<dashboard-admin v-if="isAdmin" />
<dashboard-standard v-if="isStandard" />
</div>
</template>
<script>
export default {
data () {
return {
isAdmin: false,
isStandard: false
}
},
components: {
DashboardAdmin: () => import('./DashboardAdmin.vue'),
DashboardStandard: () => import('./DashboardStandard.vue')
},
created () {
// Check user role and set one to true.
}
}
</script>

A cyclic dependency between 'router' and 'store' in Vuex app

I have a vue.js app with a router that prevents the pages from been open without authorization using the following code:
import Router from 'vue-router';
import store from '../store/index';
function guardAuth(to, from, next) {
if (store.state.authorizationToken) {
next();
} else {
next({
name: 'login',
query: { redirect: to.fullPath },
});
}
}
export default new Router({
mode: 'history',
routes: [
{
path: '/',
name: 'toroot',
redirect: 'login',
},
{
path: '/overview',
component: Overview,
beforeEnter: guardAuth,
},
....
and a store mutation that is called when an API call fails:
import axios from 'axios';
import Cookies from 'js-cookie';
import router from '../router/index';
export default new Vuex.Store({
state: {
mutations: {
handleApiFail(state, err) {
if (err && !axios.isCancel(err) && state.authorizationToken) {
// Block subsequent logout calls.
state.authorizationToken = null;
// Clear the token cookie just in case.
Cookies.set('authorizationToken', null);
// Stop the current and subsequent requests.
state.cancellationSource.cancel('Authorization token has expired.');
router.push({ name: 'login', query: { expired: '1', redirect: window.location.pathname } });
}
},
as you can see from the code above 'router' imports 'store' and 'store' imports 'router' and as far as I see this causes 'store' to be undefined inside 'guardAuth()'. Obviously, I can get rid of this cyclic dependency by moving 'handleApiFail' to a separate '.js' file, but I am not sure that it is a good idea. Is there a better solution or some common approach for haling this sutiation? Should 'handleApiFail' be a mutation or a simple function? Can a mutation use 'router'? Do I really need to get rid of the cyclic dependency (for example, in C++ I does not)?
It be better handleapi fail in separate function than mutation. and if you want to check it before entering route. you could use beforeEnter() on your route.
check this docs about beforeEnter or another route properties
Store mutation methods should not perform any logic at all. Stores are only used to hold your global application state, they should not perform any logic like authorizing the user or navigating through your application. What you'll want to do is move the logic out of the store and into the component that does the authorization check. From there just do something like $store.commit('unauthorized') and $store.commit('authorized', user). Should look like this:
sendAuthRequest.then(
(success) => {
$store.commit('authorized', <userVariable>);
$router.push(...);
}, (failure) => {
$store.commit('unauthorized');
$router.push(...);
}
);

VueRouter, VueJS, and Laravel route guard

I wanted to hide a particular page of my application behind a layer of security (a simple passcode form that will send a request to the server for validation).
Based off the documentation of VueRouter, I figured a beforeEnter would be appropriate. However, I am not entirely sure how one would require a user to access a particular component, and then successfully enter a passcode before being allowed to proceed to this current route.
Does anyone have an example of this? I am having trouble finding anything similar.
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{ path: '/test/:testURL', component: require('./components/test.vue'),
beforeEnter: (to, from, next) => {
// somehow load another component that has a form
// the form will send a request to Laravel which will apply some middleware
// if the middleware successfully resolves, this current route should go forward.
}
},
];
const router = new VueRouter({
routes,
mode: 'history',
});
const app = new Vue({
router
}).$mount('#app');
Assuming you want to perform authentication only for selected components, you can go with using beforeEnter route guard. Use the following code.
const routes = [
{ path: '/test/:testURL', component: require('./components/test.vue'),
beforeEnter:requireLogin
},
];
function requireLogin(to, from, next) {
if (authenticated) {
next(true);
} else {
next({
path: '/login',
query: {
redirect: to.fullPath
}
})
}
}
Further, you can create a login screen and action in login component to redirect to given redirect parameter after setting authenticated variable to true. I recommend you to maintain authenticated variable in the veux store

How to use Vue Router from Vuex state?

In my components I've been using:
this.$router.push({ name: 'home', params: { id: this.searchText }});
To change route. I've now moved a method into my Vuex actions, and of course this.$router no longer works. Nor does Vue.router. So, how do I call router methods from the Vuex state, please?
I'm assuming vuex-router-sync won't help here as you need the router instance.
Therefore although this doesn't feel ideal you could set the instance as a global within webpack, i.e.
global.router = new VueRouter({
routes
})
const app = new Vue({
router
...
now you should be able to: router.push({ name: 'home', params: { id: 1234 }}) from anywhere within your app
As an alternative if you don't like the idea of the above you could return a Promise from your action. Then if the action completes successfully I assume it calls a mutation or something and you can resolve the Promise. However if it fails and whatever condition the redirect needs is hit you reject the Promise.
This way you can move the routers redirect into a component that simply catches the rejected Promise and fires the vue-router push, i.e.
# vuex
actions: {
foo: ({ commit }, payload) =>
new Promise((resolve, reject) => {
if (payload.title) {
commit('updateTitle', payload.title)
resolve()
} else {
reject()
}
})
# component
methods: {
updateFoo () {
this.$store.dispatch('foo', {})
.then(response => { // success })
.catch(response => {
// fail
this.$router.push({ name: 'home', params: { id: 1234 }})
})
I a situation, I find myself to use .go instead of .push.
Sorry, no explanation about why, but in my case it worked. I leave this for future Googlers like me.
I believe rootState.router will be available in your actions, assuming you passed router as an option in your main Vue constructor.
As GuyC mentioned, I was also thinking you may be better off returning a promise from your action and routing after it resolves. In simple terms: dispatch(YOUR_ACTION).then(router.push()).
state: {
anyObj: {}, // Just filler
_router: null // place holder for router ref
},
mutations: {
/***
* All stores that have this mutation will run it
*
* You can call this in App mount, eg...
* mounted () {
* let vm = this
* vm.$store.commit('setRouter', vm.$router)
* }
*
setRouter (state, routerRef) {
state._router = routerRef
}
},
actions: {
/***
* You can then use the router like this
* ---
someAction ({ state }) {
if (state._router) {
state._router.push('/somewhere_else')
} else {
console.log('You forgot to set the router silly')
}
}
}
}
Update
After I published this answer I noticed that defining it the way I presented Typescript stopped detecting fields of state. I assume that's because I used any as a type. I probably could manually define the type, but it sounds like repeating yourself to me. That's way for now I ended up with a function instead of extending a class (I would be glad for letting me know some other solution if someone knows it).
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default (router: VueRouter) => {
return new Store({
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
import Vue from 'vue'
import App from './App.vue'
import createStore from './store'
// ...
new Vue({
router,
store: createStore(router),
render: createElement => createElement(App)
}).$mount('#app')
Initial answer content
I personally just made a wrapper for a typical Store class.
import { Store } from 'vuex'
import VueRouter from 'vue-router'
// ...
export default class extends Store<any> {
constructor (router: VueRouter) {
// router = Vue.observable(router) // You can either do that...
super({
state: {
// router // ... or add `router` to `store` if You need it to be reactive.
// ...
},
// ...
})
}
}
If You need $route You can just use router.currentRoute. Just remember You rather need router reactive if You want Your getters with router.currentRoute to work as expected.
And in "main.ts" (or ".js") I just use it with new Store(router).
import Vue from 'vue'
import App from './App.vue'
import Store from './store'
// ...
new Vue({
router,
store: new Store(router),
render: createElement => createElement(App)
}).$mount('#app')