Use Protected Routes in Vue.js
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
- Start with the first post of this series
- Or clone the codebase into a directory of your choice:
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
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
- The Vue Router can be used to create routes in the frontend application and Navigation Guards to secure specific routes.
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
- Example for using History mode:
- 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
- Example for using Hash mode:
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 (excludingpackage-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 Router
constructor.
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 theroutes
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 thelocalStorage
- 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>
Navigation Guards in Vue
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 navigatingto
from
- contains the route object of the route that we are navigatingfrom
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 theApp.vue
’screated()
method which got overwritten by adding the Vue Router. But the code in the method would be executed after registering the routes and therefore theaccessToken
property would always benull
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.