vue router 4 next() callback warning - vue.js

I have unsucessfully been trying to fix this vue-router warning :
The "next" callback was called more than once in one navigation guard when going from "/" to "/". It should be called exactly one time in each navigation guard. This will fail in production.
I have read the official doc (https://next.router.vuejs.org/guide/advanced/navigation-guards.html#global-before-guards) but I keep getting infinite next loop when making the suggested changes, so I am obviously doing something wrong.
What I would like to do is :
Have non authenticated users only be able to navigate to "/" route. and get redirected to "/" if they try to enter a protected route.
Have authenticated users directly land on "/mapping" and skip "/" route. They can then nav to any protected routes.
I verify auth with "let user = projectAuth.currentUser".
Thanks in advance for your tips
import { createRouter, createWebHistory } from 'vue-router'
import Mapping from '../views/Mapping.vue'
import Home from '../views/Home.vue'
import TestDthree from '../views/TestDthree.vue'
import Testpage1 from '../views/Testpage1.vue'
import { projectAuth } from '../firebase/config'
// auth guard or route guard
// requires to be authenticated
const requireAuth = (to, from, next) => {
let user = projectAuth.currentUser
if(!user){
next({name:'Home'})
}
next()
}
// no auth needed
// bypassed Home for any logged in user
const requireNoAuth = (to, from, next) => {
let user = projectAuth.currentUser
if(user){
next({name:'Mapping'})
}
next()
}
const routes = [
{
path: '/',
name: 'Home',
component: Home,
beforeEnter: requireNoAuth
},
{
path: '/mapping',
name: 'Mapping',
component: Mapping,
beforeEnter: requireAuth
},
{
path: '/testpage1',
name: 'Testpage1',
component: Testpage1,
beforeEnter: requireAuth
},
{
path: '/loadLegacyData2',
name: 'LoadLegacyData2',
component: () => import( '../views/LoadLegacyData2.vue'),
beforeEnter: requireAuth
},
{
path: '/testmap',
name: 'TestDthree',
component: TestDthree,
beforeEnter: requireAuth
}
]
const router = createRouter({
history: createWebHistory(process.env.BASE_URL),
routes
})
export default router

Just add returns in requireAuth:
const requireAuth = (to, from, next) => {
let user = projectAuth.currentUser
if(!user){
next({name:'Home'})
return
}
next()
return
}
Or
const requireAuth = (to, from, next) => {
next(projectAuth.currentUser ? to : {name:'Home'})
return
}

you have to check if you are not already on the page, otherwise it will loop indefinitely. Something like this
const requireAuth = (to, from, next) => {
let user = projectAuth.currentUser
if(!user && from.name !== 'Home'){
next({name:'Home'})
}
next()
}

Related

Vue router - navigation guard is skipped when using router.push()

I have a Vue SPA with i18n and some views that require authentication via navigation guard.
When i am not authenticated and go to url via my browser:
examplepage.com/en/lockedpage
i get redirected to:
examplepage.com/en/login
which is good, however when i click a button that runs:
#click="$router.push(`/${$i18n.locale}/lockedpage`)"
i get in to the page even if i am not authenticated.
I want to get redirected to the login page if not authenticated
this is my router.js:
import Vue from 'vue';
import Router from 'vue-router';
import Home2 from './views/Home2.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';
import ErrorLanding from './views/ErrorLanding.vue'
import Root from "./Root"
import i18n, { loadLocaleMessagesAsync } from "#/i18n"
import {
setDocumentLang
} from "#/util/i18n/document"
Vue.use(Router);
const { locale } = i18n
export const router = new Router({
mode: 'history',
base: '/',
routes: [
{
path: '/',
redirect: locale
},
{
path: "/:locale",
component: Root,
children: [
{
name: 'Home',
path: '',
component: Home2,
},
{
name: 'Home2',
path: '/',
component: Home2,
},
{
name: 'Login',
path: 'login',
component: Login,
},
{
path: 'register',
component: Register,
},
{
path: 'lockedpage',
name: 'lockedpage',
webpackChunkName: "lockedpage",
meta: {authRequired: true},
component: () => import('./views/LockedPage.vue')
},
{
path: '*',
component: ErrorLanding,
name: 'NotFound'
}
]
}
],
router.beforeEach((to, from, next) => {
if (to.params.locale === from.params.locale) {
next()
return
}
const { locale } = to.params
loadLocaleMessagesAsync(locale).then(() => {
setDocumentLang(locale)
const publicPages = [ `/`, `/${locale}`, `/${locale}/`];
const authRequired = !publicPages.includes(to.path);
const loggedIn = localStorage.getItem('user');
const redirect = to.path;
if (authRequired && loggedIn === null) {
if(to.meta.authRequired === false) {
next();
}
else
next({ name: 'Login', query: { redirect: redirect } });
} else {
next();
}
})
next()
});
Why does my navigationguard skip when using router.push() ?
This issue started after adding i18n, with localeredirect. so all routes comes after a locale for example: /en/.../
As Estus points out in the comments, the issue is that the first thing you're checking for is if the locales match, and if they do you're calling next() and sending the user to the page.
As outlined here:
Make sure that the next function is called exactly once in any given pass through the navigation guard. It can appear more than once, but only if the logical paths have no overlap, otherwise the hook will never be resolved or produce errors.
If you need to keep the locale check between the to and from pages, you could do something like:
if (to.params.locale === from.params.locale && loggedIn) {
next()
return
}
which will check if the loggedIn variable is truthy before pushing the user to the page they're trying to navigate to.
I believe just removing the if statement that checks if the locales match would also work.

Refreshing page redirects to home using vue router

I am facing a problem with my vuejs project. I am using vue router in a single page application. I can go to any page using vue router. But when I reload the page at any route, it redirects me to / of the project. Here is the code I have written for vue router in router/index.js file.
import Vue from 'vue'
import VueRouter from 'vue-router'
// import store from '../store'
import Home from '../views/Home.vue'
import Login from '#/components/auth/Login.vue'
import Register from '#/components/auth/Register.vue'
import Admin from '#/components/admin/Admin.vue'
import CreateCourse from '#/components/admin/course/CreateCourse.vue'
import Categories from '#/components/admin/Categories.vue'
Vue.use(VueRouter);
function isAuthenticated(to, from, next) {
// if (store.getters['auth/authenticated']) {
// next();
// } else {
// next('/login');
// }
next();
}
function isAdmin(to, from, next) {
// if (store.getters['auth/user'].role === 'super' || store.getters['auth/user'].role === 'admin') {
// next();
// } else {
// next('/');
// }
next();
}
function isNotAuthenticated(to, from, next) {
// if (!store.getters['auth/authenticated']) {
// next();
// } else {
// next('/');
// }
next();
}
const routes = [
{
path: '/',
name: 'Home',
component: Home,
},
{
path: '/about',
name: 'About',
beforeEnter: isAuthenticated,
component: () => import(/* webpackChunkName: "about" */ '../views/About.vue')
},
{
path: '/login',
name: 'Login',
component: Login,
beforeEnter: isNotAuthenticated,
},
{
path: '/register',
name: 'Register',
component: Register,
beforeEnter: isNotAuthenticated,
},
{
path: '/admin',
name: 'Admin',
component: Admin,
beforeEnter: isAdmin,
},
];
const router = new VueRouter({
mode: 'history',
routes
});
export default router
What is the problem?
Note: The commented code is to control user access to a specific route. I am calling beforeEnter for each route to check if the user has permission or not. Is there any better solution?
I found what is wrong with my code. Every time I reload the page, I authenticate the user using vuex and if the authentication is successful, I redirect the user to home page. So, every time I refresh the page, I am authenticated and redirected to the home page. Now I have removed the redirect login after login and the problem is solved.

How to disable access to login and signup page when user is logged in?

i am confused why is this not working.
Routes
const router = new VueRouter({
mode: 'history',
routes: [
{ path: '/', component: Home, name: 'Home', meta: { requiresAuth: true }},
{ path: '/adminarea', component: Admin, name:"Admin", meta: { requiresAuth: true }},
{ path: '/login', component: Login, name: 'Login'},
{ path: '/signup', component: Register, name: 'Signup'},
]
});
router.beforeEach((to, from, next) => {
if (to.matched.some(record => record.meta.requiresAuth)) {
// this route requires auth, check if logged in
// if not, redirect to login page.
if (!store.getters.getAuth) {
next({ name: 'Login' })
} else {
next();
}
} else {
next()
}
});
to.matched.some(record => record.meta.requiresAuth this line should allow only routes with meta requiresAuth but i don't know why is it allowing other routes as well.
I can manually access login and signup pages.
I cannot figure out what is wrong here.
I included a disableIfLoggedIn value in the router meta section
{
name: 'login',
path: '/login',
component: Login,
meta: {
disableIfLoggedIn: true
}
},
which will check before every route navigation
import {store} from "./store"; // import the store to check if authenticated
router.beforeEach((to, from, next) => {
// if the route is not public
if (!to.meta.public) {
// if the user authenticated
if (store.getters.isAuthenticated) { // I declared a `getter` function in the store to check if the user is authenticated.
// continue to the route
next();
} else {
// redirect to login
next({name: 'login'});
}
}
next();
});
Inside the store declare a getter function to check if authenticated or not
const getters = {
isAuthenticated: state => {
return state.isAuth; // code to check if authenticated
}
};
The meta value disableIfLoggedIn can be set to true in any of the router object. Then it won't be accessible after logged in.

Why does my Vue Router throw a Maximum call stack error?

I have a really simple routing practically looks like this I'm using this under electron
import Vue from "vue";
import VueRouter from "vue-router";
import Projects from "../views/Projects.vue";
import RegisterUser from "#/views/RegisterUser.vue";
//import { appHasOwner } from "#/services/";
Vue.use(VueRouter);
const routes = [
{
path: "/",
name: "projects",
component: Projects,
meta: {
requiresUser: true
}
},
{
path: "/register",
name: "register",
component: RegisterUser
},
{
path: "/settings",
name: "settings",
meta: {
requiresUser: true
},
// route level code-splitting
// this generates a separate chunk (about.[hash].js) for this route
// which is lazy-loaded when the route is visited.
component: () =>
import(/* webpackChunkName: "about" */ "../views/Settings.vue")
}
];
const router = new VueRouter({
mode: "history",
base: process.env.BASE_URL,
routes
});
router.beforeEach((to, from, next) => {
if (to.matched.some(route => route.meta.requiresUser === true)) {
//this will be for test case undefined
let user;
if (typeof user === "undefined") {
// eslint-disable-next-line no-console
console.log(user); //logs undefined but at the end no redirect
next("/register");
} else {
next();
}
}
});
export default router;
taking the example from the docs
// GOOD
router.beforeEach((to, from, next) => {
if (!isAuthenticated) next('/login')
else next()
})
the application can boot only if there is a user attached in database either should redirect to the register component but the code above will end with Maximum call stack size exceeded. So how to check with beforeEach conditions end redirect to a given page?
The Maximum call stack size exceeded is usually due to infinite recursion, and that certainly seems to be the case here. In router.beforeEach you're calling next to go to the /register route, which goes back into this method, which calls next, and so on. I see you have a requiresUser in your meta, so you need to check that in beforeEach, like this:
router.beforeEach((to, from, next) => {
// If the route's meta.requiresUser is true, make sure we have a user, otherwise redirect to /register
if (to.matched.some(route => route.meta.requiresUser === true)) {
if (typeof user == "undefined") {
next({ path: '/register' })
} else {
next()
}
}
// Route doesn't require a user, so go ahead
next()
}

Vue router navigation gaurd from within the component

I use vuex from centralized state management
in my vuex store.js i store the login status as a boolean value like below
export const store = new Vuex.Store({
state: {
loggedIn: false,
userName: 'Guest',
error: {
is: false,
errorMessage: ''
}
},
getters: {
g_loginStatus: state => {
return state.loggedIn;
},
g_userName: state => {
return state.userName;
},
g_error: state => {
return state.error;
}
}
)};
When the user logs in i set the loginstatus to true and remove the login button and replace it with log out button
everything works fine but the problem is when the user is logged in and if i directly enter the path to login component in the search bar i am able to navigate to login page again
I want to preent this behaviour
If the uses is logged in and searches for the path to loginpage in the searchbar he must be redirected to home page
I have tried using beforeRouteEnter in the login component
But we do not have acess to the this instance since the component is not yet loaded
So how can i check for login status from my store
my script in login.vue
script>
export default{
data(){
return{
email: '',
password: ''
};
},
methods: {
loginUser(){
this.$store.dispatch('a_logInUser', {e: this.email, p: this.password}).then(() =>{
this.$router.replace('/statuses');
});
}
},
beforeRouteEnter (to, from, next) {
next(vm => {
if(vm.$store.getters.g_loginStatus === true){
//what shall i do here
}
})
}
}
It is much better to put the navigation guards in routes not in pages/components and call the state getters on route file.
// /router/index.js
import Vue from 'vue'
import Router from 'vue-router'
import store from '../store'
// Protected Pages
import Dashboard from '#/views/dashboard'
// Public Pages
import Dashboard from '#/views/login'
Vue.use(Router)
// If you are not logged-in you will be redirected to login page
const ifNotAuthenticated = (to, from, next) => {
if (!store.getters.loggedIn) {
next()
return
}
next('/') // home or dashboard
}
// If you are logged-in/authenticated you will be redirected to home/dashboard page
const ifAuthenticated = (to, from, next) => {
if (store.getters.loggedIn) {
next()
return
}
next('/login')
}
const router = new Router({
mode: 'history',
linkActiveClass: 'open active',
scrollBehavior: () => ({ y: 0 }),
routes: [
{
path: '/',
redirect: '/dashboard',
name: 'Home',
component: Full,
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: Dashboard,
beforeEnter: ifAuthenticated
},
]
},
{
path: '/login',
name: 'Login',
component: Login,
beforeEnter: ifNotAuthenticated
},
{
path: '*',
name: 'NotFound',
component: NotFound
}
]
})
export default router
You can also use vue-router-sync package to get the value of store values
You can redirect the user to the home page or some other relevant page:
mounted () {
if(vm.$store.getters.g_loginStatus === true){
this.$router('/')
}
}
beforeRouteEnter (to, from, next) {
next(vm => {
if(vm.$store.getters.g_loginStatus === true){
next('/')
}
})
}
From the docs:
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.