Vue router guard beforeEach store user location before login and redirect after. What is right way to implement? - vue-router

I'm using vue router with quasar framework. Here is my route guard at beforeEach hook:
let storedURL = null
router.beforeEach((to, from, next) => {
const record = to.matched.find(record => record.meta.auth)
if (record) {
if (store.getters['auth/check']) {
if (storedURL) {
const redirectURL = storedURL
storedURL = null
next(redirectURL)
} else {
next()
}
} else {
storedURL = to.path
next('/')
}
} else {
if (to.path === '/' && store.getters['auth/check']) {
next('/dashboard')
} else {
next()
}
}
})
It works but throws an error when store location:
Error: Script terminated by timeout at:
notify#webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:730:36
reactiveSetter#webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:1056:11
proxySetter#webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:4625:5
__onControlFocusin#webpack-internal:///./node_modules/quasar/src/components/field/QField.js:345:9
invokeWithErrorHandling#webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:1853:26
invoker#webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:2178:14
add$1/original._wrapper#webpack-internal:///./node_modules/vue/dist/vue.runtime.esm.js:6907:25
and when redirects:
uncaught exception: undefined
Without this guard there are no errors. But I can't understand what is wrong with my code. Thanks for help!

Related

How to make if statement in beforeEach only work on loadup once?

I set up my router beforeEach, is it possible, for the slug thing to only take action on first load up of page?
For example, if I enter my page it should redirect me from default to phase-first, it works fine. But after when I try to click again on default button to redirect me to default page, it keeps redirecting me to phase-first. I've tried adding another to path default afterwards but it wont work.
router.beforeEach((to, from, next) => {
let slug = store.state.profile.currentPhase.slug;
if (to.path === '/phase/default' && slug === 'phase-first') {
return next('/phase-first');
}
if (to.path === '/phase/default') {
return next('/phase/default');
}
next();
}
How about
let redirectToFirstPhase = true;
router.beforeEach((to, from, next) => {
// let slug = store.state.profile.currentPhase.slug;
if (to.path === '/phase/default' && redirectToFirstPhase) {
redirectToFirstPhase = false;
next('/phase-first');
} else {
next();
}
})

Vuex autologin with navigation guard

Hi im working with vue and vuex trying to set a navigation route so that the user is logged in automatically in case the token is still in local storage. Im getting a rare behaviour, if I check the value of a the getter that returns the variable called isauthenticated which is true if there is a valid token I can clearly see that the value is true, but when I get that value by it self it says its false.
router.beforeEach(function (to, from, next) {
console.log(to);
console.log(from);
console.log(store.getters);
console.log("is auth "+store.getters["auth/isAuthenticated"])
if (to.meta.requiresAuth && !store.getters["auth/isAuthenticated"]) {
console.log("redirected to landing page")
next('/');
} else if (to.meta.requiresUnauth && store.getters["auth/isAuthenticated"]) {
console.log("redirected to home");
console.log(to.meta.requiresUnauth);
next('/home');
} else {
console.log("nexting")
next();
}
});
output:
{}
auth/isAuthenticated: true
auth/token: "eyJhbGciOiJSUz...."
auth/userId: "nHSQ3...."
user/username: ""
get auth/isAuthenticated: ƒ ()
get auth/token: ƒ ()
get auth/userId: ƒ ()
get user/username: ƒ ()
__proto__: Object
is auth false
nexting
the problem was the navigation before each was running before my created() hook in my app.vue I fixed it by doing this:
store.dispatch('auth/tryLogin').then(
router.beforeEach(function (to, from, next) {
if (to.meta.requiresAuth && !store.getters["auth/isAuthenticated"]) {
console.log("redirected to landing page")
next('/');
} else if (to.meta.requiresUnauth && store.getters["auth/isAuthenticated"]) {
console.log("redirected to home");
next('/home');
} else {
console.log("nexting")
next();
}
}));
You can read more about it here Router beforeEach guard executed before state loaded in Vue created()

How to solve Avoided redundant navigation to current location error in vue?

I am new in vue and i got the error after user logged in and redirect to another route.
Basically i am a PHP developer and i use laravel with vue. Please help me to solve this error.
Uncaught (in promise) Error: Avoided redundant navigation to current location: "/admin".
Here is the screenshot too
Vue Code
methods: {
loginUser() {
var data = {
email: this.userData.email,
password: this.userData.password
};
this.app.req.post("api/auth/authenticate", data).then(res => {
const token = res.data.token;
sessionStorage.setItem("chatbot_token", token);
this.$router.push("/admin");
});
}
}
Vue Routes
const routes = [
{
path: "/admin",
component: Navbar,
name: "navbar",
meta: {
authGuard: true
},
children: [
{
path: "",
component: Dashboard,
name: "dashboard"
},
{
path: "users",
component: Users,
name: "user"
}
]
},
{
path: "/login",
component: Login,
name: "login"
}
];
const router = new VueRouter({
routes,
mode: "history"
});
router.beforeEach((to, from, next) => {
const loggedInUserDetail = !!sessionStorage.getItem("chatbot_token");
if (to.matched.some(m => m.meta.authGuard) && !loggedInUserDetail)
next({ name: "login" });
else next();
});
As I remember well, you can use catch clause after this.$router.push. Then it will look like:
this.$router.push("/admin").catch(()=>{});
This allows you to only avoid the error displaying, because browser thinks the exception was handled.
I don't think suppressing all errors from router is good practice, I made just picks of certain errors, like this:
router.push(route).catch(err => {
// Ignore the vuex err regarding navigating to the page they are already on.
if (
err.name !== 'NavigationDuplicated' &&
!err.message.includes('Avoided redundant navigation to current location')
) {
// But print any other errors to the console
logError(err);
}
});
Maybe this is happening because your are trying to route to the existing $route.matched.path.
For original-poster
You may want to prevent the error by preventing a route to the same path:
if (this.$route.path != '/admin') {
this.$router.push("/admin");
}
Generic solutions
You could create a method to check for this if you are sending dynamic routes, using one of several options
Easy: Ignore the error
Hard: Compare the $route.matched against the desired route
1. Ignore the error
You can catch the NavigationDuplicated exception and ignore it.
pushRouteTo(route) {
try {
this.$router.push(route);
} catch (error) {
if (!(error instanceof NavigationDuplicated)) {
throw error;
}
}
}
Although this is much simpler, it bothers me because it generates an exception.
2. Compare the $route.matched against the desired route
You can compare the $route.matched against the desired route
pushRouteTo(route) {
// if sending path:
if (typeof(route) == "string") {
if (this.$route.path != route) {
this.$router.push(route);
}
} else { // if sending a {name: '', ...}
if (this.$route.name == route.name) {
if ('params' in route) {
let routesMatched = true;
for (key in this.$route.params) {
const value = this.$route.params[key];
if (value == null || value == undefined) {
if (key in route.params) {
if (route.params[key] != undefined && route.params[key] != null) {
routesMatched = false;
break;
}
}
} else {
if (key in route.params) {
if (routes.params[key] != value) {
routesMatched = false;
break
}
} else {
routesMatched = false;
break
}
}
if (!routesMatched) {
this.$router.push(route);
}
}
} else {
if (Object.keys(this.$route.params).length != 0) {
this.$router.push(route);
}
}
}
}
}
This is obviously a lot longer but doesn't throw an error. Choose your poison.
Runnable demo
You can try both implementations in this demo:
const App = {
methods: {
goToPageCatchException(route) {
try {
this.$router.push(route)
} catch (error) {
if (!(error instanceof NavigationDuplicated)) {
throw error;
}
}
},
goToPageMatch(route) {
if (typeof(route) == "string") {
if (this.$route.path != route) {
this.$router.push(route);
}
} else { // if sending a {name: '', ...}
if (this.$route.name == route.name) {
if ('params' in route) {
let routesMatched = true;
for (key in this.$route.params) {
const value = this.$route.params[key];
if (value == null || value == undefined) {
if (key in route.params) {
if (route.params[key] != undefined && route.params[key] != null) {
routesMatched = false;
break;
}
}
} else {
if (key in route.params) {
if (routes.params[key] != value) {
routesMatched = false;
break
}
} else {
routesMatched = false;
break
}
}
if (!routesMatched) {
this.$router.push(route);
}
}
} else {
if (Object.keys(this.$route.params).length != 0) {
this.$router.push(route);
}
}
} else {
this.$router.push(route);
}
}
},
},
template: `
<div>
<nav class="navbar bg-light">
Catch Exception
Match Route
</nav>
<router-view></router-view>
</div>`
}
const Page1 = {template: `
<div class="container">
<h1>Catch Exception</h1>
<p>We used a try/catch to get here</p>
</div>`
}
const Page2 = {template: `
<div class="container">
<h1>Match Route</h1>
<p>We used a route match to get here</p>
</div>`
}
const routes = [
{ name: 'page1', path: '/', component: Page1 },
{ name: 'page2', path: '/page2', component: Page2 },
]
const router = new VueRouter({
routes
})
new Vue({
router,
render: h => h(App)
}).$mount('#app')
<link href="https://cdn.jsdelivr.net/npm/bootstrap#4.5.3/dist/css/bootstrap.min.css" rel="stylesheet"/>
<script src="https://cdnjs.cloudflare.com/ajax/libs/vue/2.6.12/vue.min.js"></script>
<script src="https://unpkg.com/vue-router/dist/vue-router.js"></script>
<div id="app"></div>
Just to make this complete you can also compare the from and to route fullPaths and compare them against each other which seems to me a simple, valid and reusable solution.
Here is an example in a component method:
move(params){
// get comparable fullPaths
let from = this.$route.fullPath
let to = this.$router.resolve(params).route.fullPath
if(from === to) { 
// handle any error due the redundant navigation here
// or handle any other param modification and route afterwards
return
}
// route as expected
this.$router.push(params)
}
If you wanna use that you just put your route params in it like this.move({ name: 'something' }). This is the easiest way to handle the duplicate route without running into try catch syntax. And also you can have that method exported in Vue.prorotype.$move = ... which will work across the whole application.
I found the solution by adding the following code to router.js:
import router from 'vue-router';
const originalPush = router.prototype.push
router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
Provide a Typescript solution
The idea is to overwrite the router.push function. You can handle (ignore) the error in one place, instead of writing catch everywhere to handle it.
This is the function to overwrite
export declare class VueRouter {
// ...
push(
location: RawLocation,
onComplete?: Function,
onAbort?: ErrorHandler
): void
}
Here is the code
// in router/index.ts
import Vue from 'vue';
import VueRouter, { RawLocation, Route } from 'vue-router';
Vue.use(VueRouter);
const originalPush = VueRouter.prototype.push;
VueRouter.prototype.push = function push(location: RawLocation): Promise<Route> {
return new Promise((resolve, reject) => {
originalPush.call(this, location, () => {
// on complete
resolve(this.currentRoute);
}, (error) => {
// on abort
// only ignore NavigationDuplicated error
if (error.name === 'NavigationDuplicated') {
resolve(this.currentRoute);
} else {
reject(error);
}
});
});
};
// your router configs ...
you can define and use a function like this:
routerPush(path) {
if (this.$route.path !== path) {
this.$router.push(path);
}
}
It means - you want to navigate to a route that looks the same as the current one and Vue doesn’t want to trigger everything again.
methods: {
loginUser() {
var data = {
email: this.userData.email,
password: this.userData.password
};
this.app.req.post("api/auth/authenticate", data).then(res => {
const token = res.data.token;
sessionStorage.setItem("chatbot_token", token);
// Here you conditioally navigate to admin page to prevent error raised
if(this.$route.name !== "/admin") {
this.$router.push("/admin");
}
});
}
}
Clean solution ;)
Move the code to an util function. So you can replace all this.$router.push with it.
import router from '../router'; // path to the file where you defined
// your router
const goToRoute = (path) =>
if(router.currentRoute.fullPath !== path) router.push(path);
export {
goToRoute
}
In addition to the above mentioned solutions: a convenient way is to put this snippet in main.js to make it a global function
Vue.mixin({
/**
* Avoids redundand error when navigating to already active page
*/
routerPush(route) {
this.$router.push(route).catch((error) => {
if(error.name != "NavigationDuplicated") {
throw error;
}
})
},
})
Now you can call in any component:
this.routerPush('/myRoute')
Global solution
In router/index.js, after initialization
// init or import router..
/* ... your routes ... */
// error handler
const onError = (e) => {
// avoid NavigationDuplicated
if (e.name !== 'NavigationDuplicated') throw e
}
// keep original function
const _push = router.__proto__.push
// then override it
router.__proto__.push = function push (...args) {
try {
const op = _push.call(this, ...args)
if (op instanceof Promise) op.catch(onError)
return op
} catch (e) {
onError(e)
}
}
I have added the code below in the main.js file of my project and the error disappeared.
import Router from 'vue-router'
const routerPush = Router.prototype.push
Router.prototype.push = function push(location) {
return routerPush.call(this, location).catch(error => error)
};
I have experienced the same issue and when I looked for a solution I found .catch(() => {}) which is actually telling the browser that we have handled the error please don’t print the error in dev tools :) Hehehe Nice hack! but ignoring an error is not a solution I think. So what I did, I created a utility function that takes two parameters router, path, and compares it with the current route's path if both are the same it means we already on that route and it ignore the route change. So simple :)
Here is the code.
export function checkCurrentRouteAndRedirect(router, path) {
const {
currentRoute: { path: curPath }
} = router;
if (curPath !== path) router.push({ path });
}
checkCurrentRouteAndRedirect(this.$router, "/dashboard-1");
checkCurrentRouteAndRedirect(this.$router, "/dashboard-2");
I had this problem and i solve it like that.
by adding that in my router file
import Router from 'vue-router'
Vue.use(Router)
const originalPush = Router.prototype.push
Router.prototype.push = function push(location) {
return originalPush.call(this, location).catch(err => err)
}
Short solution:
if (this.$router.currentRoute.name !== 'routeName1') {
this.$router.push({
name: 'routeName2',
})
}
Late to the party, but trying to add my 10 cents. Most of the answers- including the one which is accepted are trying to hide the exception without finding the root cause which causes the error. I did have the same issue in our project and had a feeling it's something to do with Vue/ Vue router. In the end I managed to prove myself wrong and it was due to a code segment we had in App.vue to replace the route in addition to the similar logic like you in the index.ts.
this.$router.replace({ name: 'Login' })
So try to do a search and find if you are having any code which calls $router.replace OR $router.push for the route you are worried about- "/admin". Simply your code must be calling the route more than once, not the Vue magically trying to call it more than once.
I guess this answer comes in super late. Instead of catching the error I looked for a way to prevent the error the come up. Therefore I've enhanced the router by an additional function called pushSave. Probably this can be done via navGuards as well
VueRouter.prototype.pushSave = function(routeObject) {
let isSameRoute = true
if (this.currentRoute.name === routeObject.name && routeObject.params) {
for (const key in routeObject.params) {
if (
!this.currentRoute.params[key] ||
this.currentRoute.params[key] !== routeObject.params[key]
) {
isSameRoute = false
}
}
} else {
isSameRoute = false
}
if (!isSameRoute) {
this.push(routeObject)
}
}
const router = new VueRouter({
routes
})
As you've probably realized this will only work if you provide a routeObject like
this.$router.pushSave({ name: 'MyRoute', params: { id: 1 } })
So you might need to enhance it to work for strings aswell
For components <router-link>
You can create a new component instead of <router-link>.
Component name: <to-link>
<template>
<router-link :to="to" :event="to === $route.path || loading ? '' : 'click'" :class="classNames">
<slot></slot>
</router-link>
</template>
<script>
export default {
name: 'ToLink',
props: {
to: {
type: [String, Number],
default: ''
},
classNames: {
type: String,
default: ''
},
loading: {
type: Boolean,
default: false
}
}
}
</script>
For bind:
import ToLink from '#/components/to-link'
Vue.component('to-link', ToLink)
For $router.push() need create global method.
toHttpParams(obj) {
let str = ''
for (const key in obj) {
if (str !== '') {
str += '&'
}
str += key + '=' + encodeURIComponent(obj[key])
}
return str
},
async routerPush(path, queryParams = undefined, params = undefined, locale = true) {
let fullPath = this.$route.fullPath
if (!path) {
path = this.$route.path
}
if (!params) {
params = this.$route.params
}
if (queryParams && typeof queryParams === 'object' && Object.entries(queryParams).length) {
path = path + '?' + this.toHttpParams(queryParams)
}
if (path !== fullPath) {
const route = { path, params, query: queryParams }
await this.$router.push(route)
}
}
I don't understand why they no longer handle this case internally, but this is what I have implemented in our app.
If you are already on the fullPath then dont bother pushing / replacing
const origPush = Router.prototype.push;
Router.prototype.push = function(to) {
const match = this.matcher.match(to);
// console.log(match.fullPath, match)
if (match.fullPath !== this.currentRoute.fullPath) {
origPush.call(this, to);
} else {
// console.log('Already at route', match.fullPath);
}
}
const origReplace = Router.prototype.replace;
Router.prototype.replace = function(to) {
const match = this.matcher.match(to);
// console.log(match.fullPath, match)
if (match.fullPath !== this.currentRoute.fullPath) {
origReplace.call(this, to);
} else {
// console.log('Already at route', match.fullPath);
}
}
you can add a random query parameter to push object like this:
this.$router.push({path : "/admin", query : { time : Date.now()} });

vue router next() function don't work in Promise in router.beforeEach

I have the following code:
router.beforeEach((to, from, next) => {
if (to.name !== from.name) {
store
.dispatch("fetchCurrentUser")
.then(() => {
console.log('then');
// do something
next();
})
.catch(() => {
console.log('catch');
router.push("/login");
next();
});
} else {
next();
}
// next();
});
I'm trying to get the current user, and if this succeeds, then do something with this data, and if the request is not successful, then redirect the user to the login page. But next () calls do not work, I get the "then" or "catch" in the console, but the redirect does not occur and an infinite loop begins. But if I take next () from condition (commented row) the redirect works fine.
To redirect you should use next('/') or next({ path: '/' }).
From the documentation:
next: Function: this function must be called to resolve the hook. The
action depends on the arguments provided to next:
next(): move on to the next hook in the pipeline. If no hooks are
left, the navigation is confirmed.
next(false): abort the current navigation. If the browser URL was
changed (either manually by the user or via back button), it will be
reset to that of the from route.
next('/') or next({ path: '/' }): redirect to a different location.
The current navigation will be aborted and a new one will be started.
You can pass any location object to next, which allows you to specify
options like replace: true, name: 'home' and any option used in
router-link's to prop or router.push
The promise resolves after the function ends.
This means that the commented next happens regardless of the result of the promise result. Then the promise resolves and you call another next.
The bottom line is that you don't need the commented next and should just cover the promise resolve.
I was able to implement an async validation inside beforeEach, authentication in my case.
export async function onBeforeEach(to, from, next) {
let { someUserToken } = to.query
if (someUserToken) {
let nextRoute = await authenticate(to)
next(nextRoute)
} else {
const userToken = store.state.application.userToken
if (!to.meta.public && !userToken) {
next({ name: 'Forbidden' })
} else {
next()
}
}
}
async function authenticate(to) {
let { someUserToken, ...rest } = to.query
return store
.dispatch('application/authenticate', {
someUserToken
})
.then(() => {
return {
name: 'Home',
query: {
...rest
}
}
})
.catch(() => {
return { name: 'Forbidden' }
})
}
I hope this helps.

VueRouter and meta fields

According to the VueRouter documentation, it is possible to add meta fields and globally restrict routes based on their values.
After attempting an implementation as outlined, I get an error:
ReferenceError: record is not defined (line 46)
Which corresponds to this line:
if (!hasCookies(record.meta.cookies)) {
Here is the file that has the router-guard logic:
import Vue from 'vue';
import VueRouter from 'vue-router';
Vue.use(VueRouter);
const routes = [
{
path : '/authenticate/:id',
component : require ('./components/authenticate.vue'),
redirect: '/here',
},
// can only get here if the passcode has been filled out
{
path : '/client/create',
component : require('./components/createClientForm.vue'),
meta : {
cookies: ['passcode_cookie'], // need passcode to be able to create the client
redirect: '/authenticate/1' // dummy value here for now
}
},
// can only get here if passcode and client form have been completed
{
path : '/test/:id',
component : require('./components/runTest.vue'),
meta : {
cookies : ['passcode_cookie', 'client_cookie'],
redirect : '/client/create'
}
}
];
const router = new VueRouter ({
routes,
mode: 'history',
});
function hasCookies (cookies) {
return cookies.every(cookie => document.cookie.indexOf(cookie) !== -1);
}
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.cookies)) {
// this route requires cookies, check if cookies exist
if (!hasCookies(record.meta.cookies)) {
next({
path : record.meta.redirect || '/',
})
} else {
next();
}
} else {
next(); // make sure to always call next()!
}
});
const app = new Vue ({
router
}).$mount('#app');
Any idea on what I may be doing wrong?
The error is self-explanatory here. The variable record is not in scope, it's not a function parameter and it's not a global variable. record is defined only within the callback function you passed to some. It's out of scope at the point where the error occurs.
Try this instead:
const record = to.matched.find(record => record.meta.cookies && !hasCookies(record.meta.cookies));
if (record) {
next({ path: record.meta.redirect || '/' });
} else {
next();
}