Use Protected Routes in Vue.js

 
(Frontend)
 

How to use Vue Router’s Navigation Guards in combination with Vuex.


About

The code is available in a Github repository

Use Protected Routes in Vue.js

In the first Post of this series, I explained how to create a Login Component with Vue CLI, SCSS, Axios and Vuex.

In the previous Post, I explained how to persist the Access Token in the localStorage and retrieve it from there on the initial app load.

This Post will continue on the previous Post's codebasis - so make sure to

git clone https://github.com/WebDevChallenges/vue-login-persistent.git vue-protected-routes
cd vue-protected-routes/
npm install

If you want to follow along.

What we will be creating

Code Demo

In this Post, I will explain how to create both a /login and a /users route. Both of them will only be accessible to the user under specific conditions (/users for authenticated users only, /login for unauthenticated users only).

How we will accomplish this

Add the router to the project

Because we used Vue CLI to create our project, we can simply implement the Vue Router by executing the following command: vue add router.

History or Hash mode

Executing this command will prompt you if you want to use history mode instead of hash mode.

  • History mode is a HTML5 feature (also called HTML5 Routing), the server has to be configured in a way, that for all requests the server can't answer, the frontend will be served
    • Example for using History mode: https://webdevchallenges.com/login
  • Hash mode has the advantage, that the server does not have to be configured in a specific way but the route may look ugly
    • Example for using Hash mode: https://webdevchallenges.com/#login

Further information can be found here.

I chose History mode because I find it more aesthetically pleasing.

Performed changes

Adding the Vue Router to our project performed a few changes on already existing files aswell as created new files.

  • Execute git status to see, which files got modified and newly created
  • Execute git diff -- . ':(exclude)package-lock.json' to see the exact changes performed on the already existing files (excluding package-lock.json)

Use the App Component as router outlet

The Vue CLI modified the App.vue file in a way that it added a navigation to the views it created.

The user should not be able to navigate to the /users route without being authenticated and therefore we don't need a navigation.

That means that the App.vue file's template should only function as an outlet for the <router-view></router-view>

Adjust the template to look like this:

<template>
  <router-view></router-view>
</template>

Adjust the views

The Vue CLI created the src/views folder with two .vue files (About.vue and Home.vue).

As mentioned earlier, we build a /login and a /users route. Therefore we should rename these views to Login.vue and Users.vue to make it easier.

For now lets focus on getting the router to work. So remove everything from the two View files and just display some Text:

  • Login.vue:
<template>
  <p>Login</p>
</template>
  • Users.vue:
<template>
  <p>Users</p>
</template>

Adjust the router configuration

We can configure the router in the src/router.js file.

You can specify the available routes in the routes array inside of the javascript object passed to the Routerconstructor.

This is how I configured the routes:

import Vue from 'vue'
import Router from 'vue-router'

Vue.use(Router)

export default new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/login',
      name: 'login',
      component: () => import(/* webpackChunkName: "login" */ './views/Login.vue')
    },
    {
      path: '/users',
      name: 'users',
      component: () => import(/* webpackChunkName: "users" */ './views/Users.vue')
    },
    {
      path: '*',
      redirect: '/login'
    }
  ]
})

It is possible to enable route level code-splitting which generates separate chunks for routes which then will be lazy loaded when visiting that route. Declaring the component property like you can see for the first two routes will enable this feature.

That means, that only the part of the application that is used will be loaded and therefore make the loading times of the app faster.

  • The first route is the /login route, which will load the ./views/Login.vue View
  • The second route is the /user route, which will load the ./views/Users.vue View
  • The third route has a wildcard path (*) that means that it matches everything and therefore can be used as Catch-All or Fallback.

    This route will redirect to /login.

    It is important to place this route as the last element of the routes array, otherwise it might redirect even though the required route would be available.

See if the routes are working

Run npm run serve in the project folder and visit http://localhost:8080 with your browser.

  • You should be automatically redirected to /login and see the Login text
  • If you enter /users as route, you should see the Users text
  • If you enter anything else (e.g. /test) you should be redirected to /login

This is the behaviour I intended.

If it does not work for you, take a look at the commit 2479b2809c4621ff021cf3db6fcc56fd41cb7905 in the Github Repository to see what you did different.

Display the Login component

We want to display the <Login /> component in the Login View. Adjust the src/views/Login.vue file:

<template>
  <Login />
</template>

<script>
import Login from '../components/Login.vue';

export default {
  components: {
    Login
  }
}
</script>

Adjust the Login component

There are two things we can remove from the Login component. Until now we display the Login Successful message if the authentication succeeded. This will no longer be necessary because we will redirect to the /users view if it succeeded.

Therefore remove this line from the <template>:

    <p v-if="accessToken">Login Successful</p>

We also don't need the accessToken attribute any longer, so remove 'accessToken' from the ...mapState array in the computed object.

Adjust the store

Next we need to adjust the store (src/store.js).

Logout action and mutation

We want the authenticated user to be able to logout.

Add the following mutation to the store:

logout: (state) => {
  state.accessToken = null;
}

This mutation will set the accessToken state property to null.


Add the following action to the store:

logout({ commit }) {
  localStorage.removeItem('accessToken');
  commit('logout');
  router.push('/login');
}

This action will

  • remove the accessToken Item from the localStorage
  • commit the login mutation
  • navigate the user to the /login route

Redirect after successful authentication

The user should be redirected to /users after successfully authenticating.

To do this, we need to get access to our router instance. This can be aquired by importing it like this at the top of the file:

import router from './router';

A good place for redirecting the user seems to be in the doLogin action in the Promise's resolve function after committing the updateAccessToken mutation. Simply add the following line:

router.push('/users');

The complete store

import Vue from 'vue'
import Vuex from 'vuex'
import axios from 'axios'
import router from './router';

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    accessToken: null,
    loggingIn: false,
    loginError: null
  },
  mutations: {
    loginStart: state => state.loggingIn = true,
    loginStop: (state, errorMessage) => {
      state.loggingIn = false;
      state.loginError = errorMessage;
    },
    updateAccessToken: (state, accessToken) => {
      state.accessToken = accessToken;
    },
    logout: (state) => {
      state.accessToken = null;
    }
  },
  actions: {
    doLogin({ commit }, loginData) {
      commit('loginStart');

      axios.post('https://reqres.in/api/login', {
        ...loginData
      })
      .then(response => {
        localStorage.setItem('accessToken', response.data.token);
        commit('loginStop', null);
        commit('updateAccessToken', response.data.token);
        router.push('/users');
      })
      .catch(error => {
        commit('loginStop', error.response.data.error);
        commit('updateAccessToken', null);
      })
    },
    fetchAccessToken({ commit }) {
      commit('updateAccessToken', localStorage.getItem('accessToken'));
    },
    logout({ commit }) {
      localStorage.removeItem('accessToken');
      commit('logout');
      router.push('/login');
    }
  }
})

Add the logout functionality

To enable our authenticated users to logout, we can for instance add a button in the Users.vue View file which will dispatch the logout action:

<template>
  <div>
    <p>Users</p>
    <button @click=logout>Logout</button>
  </div>
</template>

<script>
import { mapActions } from 'vuex';

export default {
  methods: {
    ...mapActions([
      'logout'
    ])
  },
}
</script>

Vue's Navigation Guards enable us to execute some code before navigating to a route. That way we can either allow or prevent the navigation (by redirecting to /login instead of /users if the user is not authenticated).

These Guards can be specified by passing a callback function to the .beforeEach function of the router instance we created within the src/router.js file.

The callback function takes three parameters:

  • to - contains the route object of the route that we are navigating to
  • from - contains the route object of the route that we are navigating from
  • next - is a function which specifies if the navigation should be allowed (next();) or for instance prevented by redirecting (next('/login');)

Read more here if you are interested in a deeper explaination.

Implementing Navigation Guards

Getting access to the store

First of all we need to get access to the store because we will allow or prevent the navigation depending on the store state. We can do this by importing the instance of the Store we created in the src/store.js file:

import store from './store';

Registering the Navigation Guards

Instead of directly exporting the created instance of Router we store it in a variable:

const router = new Router({...

That way we can call the .beforeEach function after instantiating the Router as explained in the previous section:

router.beforeEach((to, from, next) => {
  store.dispatch('fetchAccessToken');
  if (to.fullPath === '/users') {
    if (!store.state.accessToken) {
      next('/login');
    }
  }
  if (to.fullPath === '/login') {
    if (store.state.accessToken) {
      next('/users');
    }
  }
  next();
});
  • In the first line of the function we dispatch the fetchAccessToken action. Remember - we did this earlier within the App.vue's created() method which got overwritten by adding the Vue Router. But the code in the method would be executed after registering the routes and therefore the accessToken property would always be null if we would fetch at that point.
  • If the to route is /users and the user is not authenticated, redirect to /login (unauthenticated users are not allowed to visit the Users View)
  • If the to route is /login and the user is authenticated, redirect to /users (authenticated users have to logout before visiting the Login View)
  • If none of the previous conditions apply, allow the navigation to the route

Export the router instance

Finally we can export the router instance:

export default router;

The complete router

import Vue from 'vue'
import Router from 'vue-router'
import store from './store';

Vue.use(Router)

const router = new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/login',
      name: 'login',
      component: () => import(/* webpackChunkName: "login" */ './views/Login.vue')
    },
    {
      path: '/users',
      name: 'users',
      component: () => import(/* webpackChunkName: "about" */ './views/Users.vue')
    },
    {
      path: '*',
      redirect: '/login'
    }
  ]
})

router.beforeEach((to, from, next) => {
  store.dispatch('fetchAccessToken');
  if (to.fullPath === '/users') {
    if (!store.state.accessToken) {
      next('/login');
    }
  }
  if (to.fullPath === '/login') {
    if (store.state.accessToken) {
      next('/users');
    }
  }
  next();
});

export default router;

Test it

Run npm run serve and visit http://localhost:8080.

Thank you for reading

I hope this helped you. If it did, make sure to follow me on social media for upcoming posts.


Write a response

Your email address will not be published. Required fields are marked *

Discussion

  • dodas says:

    Hi! For me, defining navigation guards directly in view component definition / or in router entry definition is more clear and concise, since you don’t have to repeat url:

    {
    path: ‘/users’,
    name: ‘users’,
    component: () => import(/* webpackChunkName: “about” */ ‘./views/Users.vue’),
    beforeEnter: (to, from, next) => {
    // …
    }
    },

    Cheers 🙂

    • Marc says:

      Hello dodas,

      Thanks for your comment!
      You are right. This is also a good option. Another advantage of this approach would be that the .beforeEach function would not get so big.

      Cheers