How to structure a Vue project  - Authentication

How to structure a Vue project  - Authentication

How to structure a Vue project - Authentication, For the past few years my primary focus has been on the software architecture and development of the backend services.

I’ve tried to stay away from front-end as long as possible since it’s the one area of software development where I feel mostly useless and unproductive.

So with the wish to sharpen my skills a bit I’ve decided to dig a bit into front-end development and see how it plays out. I’ve done some Angular quite a few years back so I thought I’d try the latest version but since Vue is growing in popularity and we’ve recently started using it at our company I’ve decided to give it a go. There are some quite long code snippets here, as I tried to structure this post more in a tutorial fashion — I hope you’ll still make it to the end :)

Generally when starting to work in a new framework or language I try to lookup as many best practices as possible as I prefer to start with a good structure that can be easily understood, maintained and upgraded in the future. In this post I will try to explain my thinking and combine all the knowledge I’ve obtained in the last few years with the latest and greatest web development practices.

Together we will build a simple project that handles authentication and prepare basic scaffolding to use when building the rest of the app.

We’ll be using:

  • Vue.js 2.5 and Vue-CLI
  • Vuex 3.0
  • Axios 0.18
  • Vue Router 3.0

Here is the end project structure that we’ll come up with when all is said and done. I assume that you’ve read through the Vue, Vuex and Vue Router documentation and that you understand the basics behind it — if you didn’t don’t panic, I’ll keep things simple, just don’t expect to learn everything from this post ;)

└── src
    ├── App.vue
    ├── assets
    │   └── logo.png
    ├── components
    │   └── HelloWorld.vue
    ├── main.js
    ├── router.js
    ├── services
    │   ├── api.service.js
    │   ├── storage.service.js
    │   └── user.service.js
    ├── store
    │   ├── auth.module.js
    │   └── index.js
    └── views
        ├── About.vue
        ├── Home.vue
        └── LoginView.vue

Protected pages

First let’s protect some of the URLs to be accessible only to logged in users. To do this we need to edit router.js. I’ve taken the approach where all the pages are private except the ones we mark as public directly as I believe it’s better to set the visibility by default to private and expose the routes you want accessible to public by explicitly doing so.

In the code below we’re using the meta functionality of the Vue Router — you can read about it here. We’ll also be nice to our users and redirect them to the page they tried to access after the login. For the login view we kinda want it to be accessibly only when a user is not logged in so we’ve added another flag in the meta field named onlyWhenLoggedOut.

import Vue from 'vue'
import Router from 'vue-router'
import Home from './views/Home.vue'
import LoginView from './views/LoginView.vue'
import { TokenService } from './services/storage.service'

Vue.use(Router)

const router =  new Router({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/login',
      name: 'login',
      component: LoginView,
      meta: {
        public: true,  // Allow access to even if not logged in
        onlyWhenLoggedOut: true
      }
    },
    {
      path: '/about',
      name: 'about',
      // 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/About.vue')
    },
  ]
})

router.beforeEach((to, from, next) => {
  const isPublic = to.matched.some(record => record.meta.public)
  const onlyWhenLoggedOut = to.matched.some(record => record.meta.onlyWhenLoggedOut)
  const loggedIn = !!TokenService.getToken();

  if (!isPublic && !loggedIn) {
    return next({
      path:'/login',
      query: {redirect: to.fullPath}  // Store the full path to redirect the user to after login
    });
  }

  // Do not allow user to visit login page or register page if they are logged in
  if (loggedIn && onlyWhenLoggedOut) {
    return next('/')
  }

  next();
})


export default router;

router.js

You’ll notice that we’re importing a TokenService which returns a token. The TokenService lives inside services/storage.service.js and all it does is encapsulate the logic that handles the storage and retrieval of the access token to and from localStorage.

This way we can safely migrate from local storage to cookies without having to worry that we’ll break some other service or component that accesses the local storage directly. I believe this is a good practice to follow in order to avoid future headaches. The code in the storage.service.js looks like this:

const TOKEN_KEY = 'access_token'
const REFRESH_TOKEN_KEY = 'refresh_token'

/**
 * Manage the how Access Tokens are being stored and retreived from storage.
 *
 * Current implementation stores to localStorage. Local Storage should always be
 * accessed through this instace.
**/
const TokenService = {
    getToken() {
        return localStorage.getItem(TOKEN_KEY)
    },

    saveToken(accessToken) {
        localStorage.setItem(TOKEN_KEY, accessToken)
    },

    removeToken() {
        localStorage.removeItem(TOKEN_KEY)
    },

    getRefreshToken() {
        return localStorage.getItem(REFRESH_TOKEN_KEY)
    },

    saveRefreshToken(refreshToken) {
        localStorage.setItem(REFRESH_TOKEN_KEY, refreshToken)
    },

    removeRefreshToken() {
        localStorage.removeItem(REFRESH_TOKEN_KEY)
    }

}

export { TokenService }

storage.service.js

Making API requests

We can use the same logic as we did in TokenService when it comes to API interaction. Make a base service which will do all the interaction with the network so we can easily change or upgrade stuff in the future. This is exactly what we’re trying to achieve with the api.service.js— encapsulate the Axios library so that when inevitably a new thing comes along we can return to this single service and upgrade it without having to refactor the whole application. Any other service that needs to interact with API will simply import the ApiService and issue requests via the helper methods we’ve implemented.

import axios from 'axios'
import { TokenService } from '../services/storage.service'

const ApiService = {

    init(baseURL) {
        axios.defaults.baseURL = baseURL;
    },

    setHeader() {
        axios.defaults.headers.common["Authorization"] = `Bearer ${TokenService.getToken()}`
    },

    removeHeader() {
        axios.defaults.headers.common = {}
    },

    get(resource) {
        return axios.get(resource)
    },

    post(resource, data) {
        return axios.post(resource, data)
    },

    put(resource, data) {
        return axios.put(resource, data)
    },

    delete(resource) {
        return axios.delete(resource)
    },

    /**
     * Perform a custom Axios request.
     *
     * data is an object containing the following properties:
     *  - method
     *  - url
     *  - data ... request payload
     *  - auth (optional)
     *    - username
     *    - password
    **/
    customRequest(data) {
        return axios(data)
    }
}

export default ApiService

api.service.js

You might have noticed that there is an init and setHeader function in there. We will init the ApiServiceinside the main.js to make sure that we set the header if the user refreshes the page and also set the baseURL property.

To dynamically change the URL in development, staging and production environment I’m using the Vue CLI Environment Variables.

Inside your main.js file you’d place appropriate imports and then following lines:

rawmain.js

// Set the base URL of the API
ApiService.init(process.env.VUE_APP_ROOT_API)

// If token exists set header
if (TokenService.getToken()) {
  ApiService.setHeader()
}

Okay, so by now we know how to redirect the user to a login page and we’ve done some basic boilerplate code that should help us keep a clean and maintainable project. Let’s start working on the user.service.js so we can actually make a request and see how to use our ApiService we just made.

import ApiService from './api.service'
import { TokenService } from './storage.service'


class AuthenticationError extends Error {
    constructor(errorCode, message) {
        super(message)
        this.name = this.constructor.name
        this.message = message
        this.errorCode = errorCode
    }
}

const UserService = {
    /**
     * Login the user and store the access token to TokenService. 
     * 
     * @returns access_token
     * @throws AuthenticationError 
    **/
    login: async function(email, password) {
        const requestData = {
            method: 'post',
            url: "/o/token/",
            data: {
                grant_type: 'password',
                username: email,
                password: password
            },
            auth: {
                username: process.env.VUE_APP_CLIENT_ID,
                password: process.env.VUE_APP_CLIENT_SECRET
            }
        }

        try {
            const response = await ApiService.customRequest(requestData)
            
            TokenService.saveToken(response.data.access_token)
            TokenService.saveRefreshToken(response.data.refresh_token)
            ApiService.setHeader()
            
            // NOTE: We haven't covered this yet in our ApiService 
            //       but don't worry about this just yet - I'll come back to it later
            ApiService.mount401Interceptor();

            return response.data.access_token
        } catch (error) {
            throw new AuthenticationError(error.response.status, error.response.data.detail)
        }
    },

    /**
     * Refresh the access token.
    **/
    refreshToken: async function() {
        const refreshToken = TokenService.getRefreshToken()

        const requestData = {
            method: 'post',
            url: "/o/token/",
            data: {
                grant_type: 'refresh_token',
                refresh_token: refreshToken
            },
            auth: {
                username: process.env.VUE_APP_CLIENT_ID,
                password: process.env.VUE_APP_CLIENT_SECRET
            }
        }

        try {
            const response = await ApiService.customRequest(requestData)

            TokenService.saveToken(response.data.access_token)
            TokenService.saveRefreshToken(response.data.refresh_token)
            // Update the header in ApiService
            ApiService.setHeader()

            return response.data.access_token
        } catch (error) {
            throw new AuthenticationError(error.response.status, error.response.data.detail)
        }

    },

    /**
     * Logout the current user by removing the token from storage. 
     * 
     * Will also remove `Authorization Bearer <token>` header from future requests.
    **/
    logout() {
        // Remove the token and remove Authorization header from Api Service as well 
        TokenService.removeToken()
        TokenService.removeRefreshToken()
        ApiService.removeHeader()
        
        // NOTE: Again, we'll cover the 401 Interceptor a bit later. 
        ApiService.unmount401Interceptor()
    }
}

export default UserService

export { UserService, AuthenticationError }

user.service.js

We’re implementing a UserService which has 3 methods:

  • Vue.js 2.5 and Vue-CLI
  • Vuex 3.0
  • Axios 0.18
  • Vue Router 3.0

If you’ve paid attention you’ll notice that there is a mysterious 401 interceptor logic there — we’ll cover that in a moment. It’s there just so I don’t have to include another code snippet to show you where to place it.

Should I place it in Vuex store or Component?

It seems to be a good practice to put as much of your logic into the Vuex store. Primarily this is good because you can reuse the state and business logic in different components.

For example let’s say that your app allows users to login or register in multiple places — on a dedicated login/register page or, in case of a online shop, when they checkout their basket via a popup. You would probably use a different Vue component for that UI element. By placing your state and logic in the Vuex store you would be able to reuse the state and logic and simply make a few short import statements in your Component like this:

<script>
import { mapGetters, mapActions } from "vuex";
export default {
  name: "login",
  data() {
    return {
      email: "",
      password: "",
    };
  },
  computed: {
      ...mapGetters('auth', [
          'authenticating',
          'authenticationError',
          'authenticationErrorCode'
      ])
  },
  methods: {
      ...mapActions('auth', [
          'login'
      ]),
      handleSubmit() {
          // Perform a simple validation that email and password have been typed in
          if (this.email != '' && this.password != '') {
            this.login({email: this.email, password: this.password})
            this.password = ""
          }
      }
  }
};
</script>

LoginView.html

In your Vue Component you would import logic from the Vuex Store and map state or getters to your computed values and actions to your methods. You can read a bit more about mapping here.

So how does our Vuex store look like for the user.service.js?

import { UserService, AuthenticationError } from '../services/user.service'
import { TokenService } from '../services/storage.service'
import router from '../router'


const state =  {
    authenticating: false,
    accessToken: TokenService.getToken(),
    authenticationErrorCode: 0,
    authenticationError: ''
}

const getters = {
    loggedIn: (state) => {
        return state.accessToken ? true : false
    },

    authenticationErrorCode: (state) => {
        return state.authenticationErrorCode
    },

    authenticationError: (state) => {
        return state.authenticationError
    },

    authenticating: (state) => {
        return state.authenticating
    }
}

const actions = {
    async login({ commit }, {email, password}) {
        commit('loginRequest');

        try {
            const token = await UserService.login(email, password);
            commit('loginSuccess', token)

            // Redirect the user to the page he first tried to visit or to the home view
            router.push(router.history.current.query.redirect || '/');

            return true
        } catch (e) {
            if (e instanceof AuthenticationError) {
                commit('loginError', {errorCode: e.errorCode, errorMessage: e.message})
            }

            return false
        }
    },

    logout({ commit }) {
        UserService.logout()
        commit('logoutSuccess')
        router.push('/login')
    }
}

const mutations = {
    loginRequest(state) {
        state.authenticating = true;
        state.authenticationError = ''
        state.authenticationErrorCode = 0
    },

    loginSuccess(state, accessToken) {
        state.accessToken = accessToken
        state.authenticating = false;
    },

    loginError(state, {errorCode, errorMessage}) {
        state.authenticating = false
        state.authenticationErrorCode = errorCode
        state.authenticationError = errorMessage
    },

    logoutSuccess(state) {
        state.accessToken = ''
    }
}

export const auth = {
    namespaced: true,
    state,
    getters,
    actions,
    mutations
}

auth.module.js

This pretty much covers everything that you need to setup your project in a way that should hopefully help you keep things clean and maintainable.

Fetching more data from API should be easy now — simply make a new .service.js inside services, write helper methods and access API via the ApiService we made. To display this data make a Vuex Store and store the API responses in the state — use it in your Component via the mapState and mapActions. This way you’ll be able to reuse the logic in the future should you need to display or manipulate the the same data in a different component.

While I’m not a Vue expert (yet) I do like to think I know a thing or two about software architecture and I hope that this post contains some useful ideas and concepts that you can use in your next project!

Extra: How to refresh expired access token?

What’s a bit harder and skipped by so many tutorials when it comes to authentication is handling token refresh or 401 errors. There are some use-cases where it’s good to simply logout the user when a 401 error happens, but let’s see how you can refresh the access token without interrupting the user experience. So here is the 401 Interceptor we’ve had in the above code samples mentioned already.

In our ApiService we’ll add the following code to mount the Axios response interceptor.

...

import { store } from '../store'

const ApiService = {

    // Stores the 401 interceptor position so that it can be later ejected when needed
    _401interceptor: null,

    ...

    mount401Interceptor() {
        this._401interceptor = axios.interceptors.response.use(
            (response) => {
                return response
            },
            async (error) => {
                if (error.request.status == 401) {
                    if (error.config.url.includes('/o/token/')) {
                        // Refresh token has failed. Logout the user
                        store.dispatch('auth/logout')
                        throw error
                    } else {
                        // Refresh the access token
                        try{
                            await store.dispatch('auth/refreshToken')
                            // Retry the original request
                            return this.customRequest({
                                method: error.config.method,
                                url: error.config.url,
                                data: error.config.data
                            })
                        } catch (e) {
                            // Refresh has failed - reject the original request
                            throw error
                        }
                    }
                }

                // If error was not 401 just reject as is
                throw error
            }
        )
    },

    unmount401Interceptor() {
        // Eject the interceptor
        axios.interceptors.response.eject(this._401interceptor)
    }
}

api.service.js

What the code above does is intercept every API response and check if the status of the response is 401. If it is, we’re checking if 401 occurred on the token refresh call itself (we don’t want to be caught in the loop of refreshing token forever!). The code then refreshes the token and retries the request that has failed and returns the response back to the caller.

We’re dispatching a call to the Vuex store here to perform the token refresh. The code we need to add to our auth.module.js is:

const state =  {
    ...

    refreshTokenPromise: null  // Holds the promise of the refresh token
}


const actions = {
  
    ...
  
    refreshToken({ commit, state }) {
        // If this is the first time the refreshToken has been called, make a request
        // otherwise return the same promise to the caller
        if(!state.refreshTokenPromise) {
            const p = UserService.refreshToken()
            commit('refreshTokenPromise', p)

            // Wait for the UserService.refreshToken() to resolve. On success set the token and clear promise
            // Clear the promise on error as well.
            p.then(
                response => {
                    commit('refreshTokenPromise', null)
                    commit('loginSuccess', response)
                },
                error => {
                    commit('refreshTokenPromise', null)
                }
            )
        }

        return state.refreshTokenPromise
    }
}

const mutations = {
    
    ...

    refreshTokenPromise(state, promise) {
        state.refreshTokenPromise = promise
    }
}

auth.module.js

You app will probably perform several API requests to obtain the data it needs to display. Should the access token expire all of the requests will fail and therefore trigger the token refresh inside the 401 interceptor. That would in term refresh the token for each request and that is not a good thing.

There are solutions out there that queue the requests when 401 happens and process them in a queue, but the code above, at least to me, provides a more elegant solution. By saving the refresh token promise and returning the same promise to every refresh token request we’re making sure that the token gets refreshed only once.

You’ll also need to mount the 401 interceptor in your main.js as well right after you set the request header.

Recommended Courses:

Vuejs 2 + Vuex con TypeScript Nivel PRO

Curso de Vuejs 2, Cognito y GraphQL

Learn VueJS from Scratch: The Complete 1 Hour Crash Course!

SSR con Vuejs, Vuex y Quasar Framework

Curso Avanzado de Vuejs 2, Vuex y AdonisJs 4

Top Vue.js Developers in USA

Top Vue.js Developers in USA

Vue.js is an extensively popular JavaScript framework with which you can create powerful as well as interactive interfaces. Vue.js is the best framework when it comes to building a single web and mobile apps.

We, at HireFullStackDeveloperIndia, implement the right strategic approach to offer a wide variety through customized Vue.js development services to suit your requirements at most competitive prices.

Vue.js is an open-source JavaScript framework that is incredibly progressive and adoptive and majorly used to build a breathtaking user interface. Vue.js is efficient to create advanced web page applications.

Vue.js gets its strength from the flexible JavaScript library to build an enthralling user interface. As the core of Vue.js is concentrated which provides a variety of interactive components for the web and gives real-time implementation. It gives freedom to developers by giving fluidity and eases the integration process with existing projects and other libraries that enables to structure of a highly customizable application.

Vue.js is a scalable framework with a robust in-build stack that can extend itself to operate apps of any proportion. Moreover, vue.js is the best framework to seamlessly create astonishing single-page applications.

Our Vue.js developers have gained tremendous expertise by delivering services to clients worldwide over multiple industries in the area of front-end development. Our adept developers are experts in Vue development and can provide the best value-added user interfaces and web apps.

We assure our clients to have a prime user interface that reaches end-users and target the audience with the exceptional user experience across a variety of devices and platforms. Our expert team of developers serves your business to move ahead on the path of success, where your enterprise can have an advantage over others.

Here are some key benefits that you can avail when you decide to hire vue.js developers in USA from HireFullStackDeveloperIndia:

  • A team of Vue.js developers of your choice
  • 100% guaranteed client satisfaction
  • Integrity and Transparency
  • Free no-obligation quote
  • Portal development solutions
  • Interactive Dashboards over a wide array of devices
  • Vue.js music and video streaming apps
  • Flexible engagement model
  • A free project manager with your team
  • 24*7 communication with your preferred means

If you are looking to hire React Native developers in USA, then choosing HireFullStackDeveloperIndia would be the best as we offer some of the best talents when it comes to Vue.js.

How To Publish Your Vue.js Component On NPM - Vue.js Developers

How To Publish Your Vue.js Component On NPM - Vue.js Developers

How To Publish Your Vue.js Component On NPM. You’ve made an awesome component with Vue.js that you think other developers could use in their projects. How can you share it with them?

How to build Vue.js JWT Authentication with Vuex and Vue Router

How to build Vue.js JWT Authentication with Vuex and Vue Router

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication

In this tutorial, we’re gonna build a Vue.js with Vuex and Vue Router Application that supports JWT Authentication. I will show you:

  • JWT Authentication Flow for User Signup & User Login
  • Project Structure for Vue.js Authentication with Vuex & Vue Router
  • How to define Vuex Authentication module
  • Creating Vue Authentication Components with Vuex Store & VeeValidate
  • Vue Components for accessing protected Resources
  • How to add a dynamic Navigation Bar to Vue App

Let’s explore together.

Contents

Overview of Vue JWT Authentication example

We will build a Vue application in that:

  • There are Login/Logout, Signup pages.
  • Form data will be validated by front-end before being sent to back-end.
  • Depending on User’s roles (admin, moderator, user), Navigation Bar changes its items automatically.

Screenshots

– Signup Page:

– Login Page & Profile Page (for successful Login):

– Navigation Bar for Admin account:

Demo

This is full Vue JWT Authentication App demo (with form validation, check signup username/email duplicates, test authorization with 3 roles: Admin, Moderator, User). In the video, we use Spring Boot for back-end REST APIs.

Flow for User Registration and User Login

For JWT Authentication, we’re gonna call 2 endpoints:

  • POST api/auth/signup for User Registration
  • POST api/auth/signin for User Login

You can take a look at following flow to have an overview of Requests and Responses Vue Client will make or receive.

Vue Client must add a JWT to HTTP Authorization Header before sending request to protected resources.

Vue App Component Diagram with Vuex & Vue Router

Now look at the diagram below.

Let’s think about it.

– The App component is a container with Router. It gets app state from Vuex store/auth. Then the navbar now can display based on the state. App component also passes state to its child components.

Login & Register components have form for submission data (with support of vee-validate). We call Vuex store dispatch() function to make login/register actions.

– Our Vuex actions call auth.service methods which use axios to make HTTP requests. We also store or get JWT from Browser Local Storage inside these methods.

Home component is public for all visitor.

Profile component get user data from its parent component and display user information.

BoardUser, BoardModerator, BoardAdmin components will be displayed by Vuex state user.roles. In these components, we use user.service to get protected resources from API.

user.service uses auth-header() helper function to add JWT to HTTP Authorization header. auth-header() returns an object containing the JWT of the currently logged in user from Local Storage.

Technology

We will use these modules:

  • vue: 2.6.10
  • vue-router: 3.0.3
  • vuex: 3.0.1
  • axios: 0.19.0
  • vee-validate: 2.2.15
  • bootstrap: 4.3.1
  • vue-fontawesome: 0.1.7
Project Structure

This is folders & files structure for our Vue application:

With the explaination in diagram above, you can understand the project structure easily.

Setup Vue App modules

Run following command to install neccessary modules:

npm install vue-router
npm install vuex
npm install [email protected]
npm install axios
npm install bootstrap jquery popper.js
npm install @fortawesome/fontawesome-svg-core @fortawesome/free-solid-svg-icons @fortawesome/vue-fontawesome

After the installation is done, you can check dependencies in package.json file.

"dependencies": {
  "@fortawesome/fontawesome-svg-core": "^1.2.25",
  "@fortawesome/free-solid-svg-icons": "^5.11.2",
  "@fortawesome/vue-fontawesome": "^0.1.7",
  "axios": "^0.19.0",
  "bootstrap": "^4.3.1",
  "core-js": "^2.6.5",
  "jquery": "^3.4.1",
  "popper.js": "^1.15.0",
  "vee-validate": "^2.2.15",
  "vue": "^2.6.10",
  "vue-router": "^3.0.3",
  "vuex": "^3.0.1"
},

Open src/main.js, add code below:

import Vue from 'vue';
import App from './App.vue';
import { router } from './router';
import store from './store';
import 'bootstrap';
import 'bootstrap/dist/css/bootstrap.min.css';
import VeeValidate from 'vee-validate';
import { library } from '@fortawesome/fontawesome-svg-core';
import { FontAwesomeIcon } from '@fortawesome/vue-fontawesome';
import {
  faHome,
  faUser,
  faUserPlus,
  faSignInAlt,
  faSignOutAlt
} from '@fortawesome/free-solid-svg-icons';

library.add(faHome, faUser, faUserPlus, faSignInAlt, faSignOutAlt);

Vue.config.productionTip = false;

Vue.use(VeeValidate);
Vue.component('font-awesome-icon', FontAwesomeIcon);

new Vue({
  router,
  store,
  render: h => h(App)
}).$mount('#app');

You can see that we import and apply in Vue object:
store for Vuex (implemented later in src/store)
router for Vue Router (implemented later in src/router.js)
bootstrap with CSS
vee-validate
vue-fontawesome for icons (used later in nav)

Create Services

We create two services in src/services folder:


services

auth-header.js

auth.service.js (Authentication service)

user.service.js (Data service)


Authentication service

The service provides three important methods with the help of axios for HTTP requests & reponses:

  • login(): POST {username, password} & save JWT to Local Storage
  • logout(): remove JWT from Local Storage
  • register(): POST {username, email, password}
import axios from 'axios';

const API_URL = 'http://localhost:8080/api/auth/';

class AuthService {
  login(user) {
    return axios
      .post(API_URL + 'signin', {
        username: user.username,
        password: user.password
      })
      .then(this.handleResponse)
      .then(response => {
        if (response.data.accessToken) {
          localStorage.setItem('user', JSON.stringify(response.data));
        }

        return response.data;
      });
  }

  logout() {
    localStorage.removeItem('user');
  }

  register(user) {
    return axios.post(API_URL + 'signup', {
      username: user.username,
      email: user.email,
      password: user.password
    });
  }

  handleResponse(response) {
    if (response.status === 401) {
      this.logout();
      location.reload(true);

      const error = response.data && response.data.message;
      return Promise.reject(error);
    }

    return Promise.resolve(response);
  }
}

export default new AuthService();

If login request returns 401 status (Unauthorized), that means, JWT was expired or no longer valid, we will logout the user (remove JWT from Local Storage).

Data service

We also have methods for retrieving data from server. In the case we access protected resources, the HTTP request needs Authorization header.

Let’s create a helper function called authHeader() inside auth-header.js:

export default function authHeader() {
  let user = JSON.parse(localStorage.getItem('user'));

  if (user && user.accessToken) {
    return { Authorization: 'Bearer ' + user.accessToken };
  } else {
    return {};
  }
}

It checks Local Storage for user item.
If there is a logged in user with accessToken (JWT), return HTTP Authorization header. Otherwise, return an empty object.

Now we define a service for accessing data in user.service.js:

import axios from 'axios';
import authHeader from './auth-header';

const API_URL = 'http://localhost:8080/api/test/';

class UserService {
  getPublicContent() {
    return axios.get(API_URL + 'all');
  }

  getUserBoard() {
    return axios.get(API_URL + 'user', { headers: authHeader() });
  }

  getModeratorBoard() {
    return axios.get(API_URL + 'mod', { headers: authHeader() });
  }

  getAdminBoard() {
    return axios.get(API_URL + 'admin', { headers: authHeader() });
  }
}

export default new UserService();

You can see that we add a HTTP header with the help of authHeader() function when requesting authorized resource.

Define Vuex Authentication module

We put Vuex module for authentication in src/store folder.


store

auth.module.js (authentication module)

index.js (Vuex Store that contains also modules)


Now open index.js file, import auth.module to main Vuex Store here.

import Vue from 'vue';
import Vuex from 'vuex';

import { auth } from './auth.module';

Vue.use(Vuex);

export default new Vuex.Store({
  modules: {
    auth
  }
});

Then we start to define Vuex Authentication module that contains:

  • state: { status, user }
  • actions: { login, logout, register }
  • mutations: { loginSuccess, loginFailure, logout, registerSuccess, registerFailure }

We use AuthService which is defined above to make authentication requests.

auth.module.js

import AuthService from '../services/auth.service';

const user = JSON.parse(localStorage.getItem('user'));
const initialState = user
  ? { status: { loggedIn: true }, user }
  : { status: {}, user: null };

export const auth = {
  namespaced: true,
  state: initialState,
  actions: {
    login({ commit }, user) {
      return AuthService.login(user).then(
        user => {
          commit('loginSuccess', user);
          return Promise.resolve(user);
        },
        error => {
          commit('loginFailure');
          return Promise.reject(error.response.data);
        }
      );
    },
    logout({ commit }) {
      AuthService.logout();
      commit('logout');
    },
    register({ commit }, user) {
      return AuthService.register(user).then(
        response => {
          commit('registerSuccess');
          return Promise.resolve(response.data);
        },
        error => {
          commit('registerFailure');
          return Promise.reject(error.response.data);
        }
      );
    }
  },
  mutations: {
    loginSuccess(state, user) {
      state.status = { loggedIn: true };
      state.user = user;
    },
    loginFailure(state) {
      state.status = {};
      state.user = null;
    },
    logout(state) {
      state.status = {};
      state.user = null;
    },
    registerSuccess(state) {
      state.status = {};
    },
    registerFailure(state) {
      state.status = {};
    }
  }
};

You can find more details about Vuex at Vuex Guide.

Create Vue Authentication Components

Define User model

To make code clear and easy to read, we define the User model first.
Under src/models folder, create user.js like this.

export default class User {
  constructor(username, email, password) {
    this.username = username;
    this.email = email;
    this.password = password;
  }
}

Let’s continue with Authentication Components.
Instead of using axios or AuthService directly, these Components should work with Vuex Store:
– getting status with this.$store.state.auth
– making request by dispatching an action: this.$store.dispatch()


views

Login.vue

Register.vue

Profile.vue


Vue Login Page

In src/views folder, create Login.vue file with following code:

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleLogin">
        <div class="form-group">
          <label for="username">Username</label>
          <input
            type="text"
            class="form-control"
            name="username"
            v-model="user.username"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('username')"
          >Username is required!</div>
        </div>
        <div class="form-group">
          <label for="password">Password</label>
          <input
            type="password"
            class="form-control"
            name="password"
            v-model="user.password"
            v-validate="'required'"
          />
          <div
            class="alert alert-danger"
            role="alert"
            v-if="errors.has('password')"
          >Password is required!</div>
        </div>
        <div class="form-group">
          <button class="btn btn-primary btn-block" :disabled="loading">
            <span class="spinner-border spinner-border-sm" v-show="loading"></span>
            <span>Login</span>
          </button>
        </div>
        <div class="form-group">
          <div class="alert alert-danger" role="alert" v-if="message">{{message}}</div>
        </div>
      </form>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'login',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', ''),
      loading: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleLogin() {
      this.loading = true;
      this.$validator.validateAll();

      if (this.errors.any()) {
        this.loading = false;
        return;
      }

      if (this.user.username && this.user.password) {
        this.$store.dispatch('auth/login', this.user).then(
          () => {
            this.$router.push('/profile');
          },
          error => {
            this.loading = false;
            this.message = error.message;
          }
        );
      }
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

This page has a Form with username & password. We use [VeeValidate 2.x](http://<a href=) to validate input before submitting the form. If there is an invalid field, we show the error message.

We check user logged in status using Vuex Store: this.$store.state.auth.status.loggedIn. If the status is true, we use Vue Router to direct user to Profile Page:

created() {
  if (this.loggedIn) {
    this.$router.push('/profile');
  }
},

In the handleLogin() function, we dispatch 'auth/login' Action to Vuex Store. If the login is successful, go to Profile Page, otherwise, show error message.

Vue Register Page

This page is similar to Login Page.

For form validation, we have some more details:

  • username: required|min:3|max:20
  • email: required|email|max:50
  • password: required|min:6|max:40

For form submission, we dispatch 'auth/register' Vuex Action.

src/views/Register.vue

<template>
  <div class="col-md-12">
    <div class="card card-container">
      <img
        id="profile-img"
        src="//ssl.gstatic.com/accounts/ui/avatar_2x.png"
        class="profile-img-card"
      />
      <form name="form" @submit.prevent="handleRegister">
        <div v-if="!successful">
          <div class="form-group">
            <label for="username">Username</label>
            <input
              type="text"
              class="form-control"
              name="username"
              v-model="user.username"
              v-validate="'required|min:3|max:20'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('username')"
            >{{errors.first('username')}}</div>
          </div>
          <div class="form-group">
            <label for="email">Email</label>
            <input
              type="email"
              class="form-control"
              name="email"
              v-model="user.email"
              v-validate="'required|email|max:50'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('email')"
            >{{errors.first('email')}}</div>
          </div>
          <div class="form-group">
            <label for="password">Password</label>
            <input
              type="password"
              class="form-control"
              name="password"
              v-model="user.password"
              v-validate="'required|min:6|max:40'"
            />
            <div
              class="alert-danger"
              v-if="submitted && errors.has('password')"
            >{{errors.first('password')}}</div>
          </div>
          <div class="form-group">
            <button class="btn btn-primary btn-block">Sign Up</button>
          </div>
        </div>
      </form>

      <div
        class="alert"
        :class="successful ? 'alert-success' : 'alert-danger'"
        v-if="message"
      >{{message}}</div>
    </div>
  </div>
</template>

<script>
import User from '../models/user';

export default {
  name: 'register',
  computed: {
    loggedIn() {
      return this.$store.state.auth.status.loggedIn;
    }
  },
  data() {
    return {
      user: new User('', '', ''),
      submitted: false,
      successful: false,
      message: ''
    };
  },
  mounted() {
    if (this.loggedIn) {
      this.$router.push('/profile');
    }
  },
  methods: {
    handleRegister() {
      this.message = '';
      this.submitted = true;
      this.$validator.validate().then(valid => {
        if (valid) {
          this.$store.dispatch('auth/register', this.user).then(
            data => {
              this.message = data.message;
              this.successful = true;
            },
            error => {
              this.message = error.message;
              this.successful = false;
            }
          );
        }
      });
    }
  }
};
</script>

<style scoped>
label {
  display: block;
  margin-top: 10px;
}

.card-container.card {
  max-width: 350px !important;
  padding: 40px 40px;
}

.card {
  background-color: #f7f7f7;
  padding: 20px 25px 30px;
  margin: 0 auto 25px;
  margin-top: 50px;
  -moz-border-radius: 2px;
  -webkit-border-radius: 2px;
  border-radius: 2px;
  -moz-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  -webkit-box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
  box-shadow: 0px 2px 2px rgba(0, 0, 0, 0.3);
}

.profile-img-card {
  width: 96px;
  height: 96px;
  margin: 0 auto 10px;
  display: block;
  -moz-border-radius: 50%;
  -webkit-border-radius: 50%;
  border-radius: 50%;
}
</style>

Profile Page

This page gets current User from Vuex Store and show information. If the User is not logged in, it directs to Login Page.

src/views/Profile.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>
        <strong>{{currentUser.username}}</strong> Profile
      </h3>
    </header>
    <p>
      <strong>Token:</strong>
      {{currentUser.accessToken.substring(0, 20)}} ... {{currentUser.accessToken.substr(currentUser.accessToken.length - 20)}}
    </p>
    <p>
      <strong>Id:</strong>
      {{currentUser.id}}
    </p>
    <p>
      <strong>Email:</strong>
      {{currentUser.email}}
    </p>
    <strong>Authorities:</strong>
    <ul>
      <li v-for="(role,index) in currentUser.roles" :key="index">{{role}}</li>
    </ul>
  </div>
</template>

<script>
export default {
  name: 'profile',
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    }
  },
  mounted() {
    if (!this.currentUser) {
      this.$router.push('/login');
    }
  }
};
</script>

Create Vue Components for accessing Resources

These components will use UserService to request data.


views

Home.vue

BoardAdmin.vue

BoardModerator.vue

BoardUser.vue


Home Page

This is a public page.

src/views/Home.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'home',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getPublicContent().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Role-based Pages

We have 3 pages for accessing protected data:

  • BoardUser page calls UserService.getUserBoard()
  • BoardModerator page calls UserService.getModeratorBoard()
  • BoardAdmin page calls UserService.getAdminBoard()

This is an example, other Page are similar to this Page.

src/views/BoardUser.vue

<template>
  <div class="container">
    <header class="jumbotron">
      <h3>{{content}}</h3>
    </header>
  </div>
</template>

<script>
import UserService from '../services/user.service';

export default {
  name: 'user',
  data() {
    return {
      content: ''
    };
  },
  mounted() {
    UserService.getUserBoard().then(
      response => {
        this.content = response.data;
      },
      error => {
        this.content = error.response.data.message;
      }
    );
  }
};
</script>

Define Routes for Vue Router

Now we define all routes for our Vue Application.

src/router.js

import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Login from './views/Login.vue';
import Register from './views/Register.vue';

Vue.use(Router);

export const router = new Router({
  mode: 'history',
  routes: [
    {
      path: '/',
      name: 'home',
      component: Home
    },
    {
      path: '/home',
      component: Home
    },
    {
      path: '/login',
      component: Login
    },
    {
      path: '/register',
      component: Register
    },
    {
      path: '/profile',
      name: 'profile',
      // lazy-loaded
      component: () => import('./views/Profile.vue')
    },
    {
      path: '/admin',
      name: 'admin',
      // lazy-loaded
      component: () => import('./views/BoardAdmin.vue')
    },
    {
      path: '/mod',
      name: 'moderator',
      // lazy-loaded
      component: () => import('./views/BoardModerator.vue')
    },
    {
      path: '/user',
      name: 'user',
      // lazy-loaded
      component: () => import('./views/BoardUser.vue')
    }
  ]
});

Add Navigation Bar to Vue App

This is the root container for our application that contains navigation bar. We will add router-view here.

src/App.vue

<template>
  <div id="app">
    <nav class="navbar navbar-expand navbar-dark bg-dark">
      <a href="#" class="navbar-brand">bezKoder</a>
      <div class="navbar-nav mr-auto">
        <li class="nav-item">
          <a href="/home" class="nav-link">
            <font-awesome-icon icon="home" /> Home
          </a>
        </li>
        <li class="nav-item" v-if="showAdminBoard">
          <a href="/admin" class="nav-link">Admin Board</a>
        </li>
        <li class="nav-item" v-if="showModeratorBoard">
          <a href="/mod" class="nav-link">Moderator Board</a>
        </li>
        <li class="nav-item">
          <a href="/user" class="nav-link" v-if="currentUser">User</a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="!currentUser">
        <li class="nav-item">
          <a href="/register" class="nav-link">
            <font-awesome-icon icon="user-plus" /> Sign Up
          </a>
        </li>
        <li class="nav-item">
          <a href="/login" class="nav-link">
            <font-awesome-icon icon="sign-in-alt" /> Login
          </a>
        </li>
      </div>

      <div class="navbar-nav ml-auto" v-if="currentUser">
        <li class="nav-item">
          <a href="/profile" class="nav-link">
            <font-awesome-icon icon="user" />
            {{currentUser.username}}
          </a>
        </li>
        <li class="nav-item">
          <a href class="nav-link" @click="logOut">
            <font-awesome-icon icon="sign-out-alt" /> LogOut
          </a>
        </li>
      </div>
    </nav>

    <div class="container">
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  computed: {
    currentUser() {
      return this.$store.state.auth.user;
    },
    showAdminBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_ADMIN');
      }

      return false;
    },
    showModeratorBoard() {
      if (this.currentUser) {
        return this.currentUser.roles.includes('ROLE_MODERATOR');
      }

      return false;
    }
  },
  methods: {
    logOut() {
      this.$store.dispatch('auth/logout');
      this.$router.push('/login');
    }
  }
};
</script>

Our navbar looks more professional when using font-awesome-icon.
We also make the navbar dynamically change by current User’s roles which are retrieved from Vuex Store state.

Handle Unauthorized Access

If you want to check Authorized status everytime a navigating action is trigger, just add router.beforeEach() at the end of src/router.js like this:

router.beforeEach((to, from, next) => {
  const publicPages = ['/login', '/home'];
  const authRequired = !publicPages.includes(to.path);
  const loggedIn = localStorage.getItem('user');

  // try to access a restricted page + not logged in
  if (authRequired && !loggedIn) {
    return next('/login');
  }

  next();
});

Conclusion

Congratulation!

Today we’ve done so many interesting things. I hope you understand the overall layers of our Vue application, and apply it in your project at ease. Now you can build a front-end app that supports JWT Authentication with Vue.js, Vuex and Vue Router.

Happy learning, see you again!