Dev

Laravel & Vue.js - Building a Todo list, Part 5: Authentication with Vuex and Vue-router

Profil Picture

Guillaume Briday

6 minutes

Login page of Todolist front-end
Login page of Todolist front-end

Now that we have set up the development and production environments for the application, we can create our first interface and component for authentication.

Before going further, since the previous article, I added a plugin to delete the contents of the dist folder before generating our application in production. Without this, we ended up with multiple CSS and JS files, differentiated by their hash, in the same folder. It wasn't very clean, but it's now fixed.

+ var CleanWebpackPlugin = require('clean-webpack-plugin')

// ...

if (inProduction) {
  module.exports.plugins = (module.exports.plugins || []).concat([
    // ...
+    new CleanWebpackPlugin(['dist']),
    // ...
    new webpack.LoaderOptionsPlugin({
      minimize: true
    }),
  ])
}

Let's get to the heart of the matter!

What we are going to do

Make no mistake, there are plenty of interesting things presented in this video!

Vuex

Vuex will allow us to save the states of our application and share this information between components.

Vuex Data Flow
Vuex Data Flow

To put it simply, we will dispatch actions that will execute code. Then, via a commit, we will make a mutation that will modify a state. In our case, we will use it to manage JWTs and save two states: logged in and not logged in.

In a store folder, I will create two files. One for the general configuration of Vuex and another for authentication-specific configuration.

// store/index.js

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

import auth from './auth'

Vue.use(Vuex)

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

And it needs to be added to the Vue instance in app.js:

+import store from './store'

/* eslint-disable no-new */
new Vue({
  el: '#app',
  router,
+  store,
  template: '<App/>',
  components: { App }
})

And in store/auth.js:

import router from '../router'

// We define the possible mutation types
const types = {
  LOGIN: 'LOGIN',
  LOGOUT: 'LOGOUT',
}

// We define the possible states
const state = {
  logged: !!localStorage.getItem('token'),
}

// We define our mutations
const mutations = {
  [types.LOGIN](state) {
    state.logged = true
  },

  [types.LOGOUT](state) {
    state.logged = false
  },
}

// We define the getters that can be called in other components
// to get information
const getters = {
  isLogged: (state) => state.logged,
}

const actions = {
  // We take the token as a parameter
  login({ commit }, token) {
    // We change the state
    commit(types.LOGIN)

    // We save the token in localStorage that we will use in axios
    localStorage.setItem('token', token)

    // We redirect to Home
    router.push({ name: 'Home' })
  },

  logout({ commit }) {
    commit(types.LOGOUT)
    localStorage.removeItem('token')

    router.push({ name: 'Login' })
  },
}

export default {
  state,
  mutations,
  getters,
  actions,
}

We can now use our store this way, anywhere in our Vue instance:

this.$store.dispatch('login', response.data.access_token)

// or

this.$store.dispatch('logout')

Now that we have our JWT in localStorage, we can use it with each request in axios. In the js/bootstrap.js file, we will add three pieces of information:

const API_URL = process.env.API_URL || 'http://localhost/api/v1/'

axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
axios.defaults.headers.common['Authorization'] = 'Bearer ' + localStorage.token
axios.defaults.baseURL = API_URL

If the token doesn't exist, it's not a problem to define the Authorization header. It will be used but handled as an invalid token in Laravel.

Vue router

First of all, you can notice, by looking at the URL, that there is no # just after the domain name.

This is possible with the HTML5 History Mode which will simulate a real URL, but is not compatible with all browsers (especially IE).

let router = new VueRouter({
+  mode: 'history',
  routes: [
    {

I have transferred all my routes into a Navbar component that I display only when the user is authenticated.

My component looks like this:

<template>
  <nav class="bg-indigo" v-if="isLogged">
    <div class="container mx-auto px-8 py-4">
      <div class="flex justify-between">
        <div>
          <router-link
            class="text-white no-underline font-bold text-3xl hover:underline"
            to="/"
            exact
          >
            Todolist
          </router-link>
        </div>
        <div>
          <button
            @click="logout"
            class="text-grey-light mr-3 border border-white py-2 px-4 rounded hover:bg-white hover:text-indigo"
          >
            Logout
          </button>
        </div>
      </div>
    </div>
  </nav>
</template>

<script>
  import { mapGetters } from 'vuex'

  export default {
    name: 'Navbar',

    computed: mapGetters(['isLogged']),

    methods: {
      logout() {
        this.$store.dispatch('logout')
      },
    },
  }
</script>

I can use the isLogged method directly as a standard computed method.

That's what I'm doing with the condition v-if="isLogged".

Simply use the Navbar component in the main App component.

<template>
  <div>
    <navbar></navbar>

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

<script>
  export default {}
</script>

Login and TailWind CSS

The main Login component:

<template>
  <div class="h-screen flex justify-center items-center">
    <div class="w-full max-w-xs">
      <h1 class="text-center mb-6">Todolist</h1>

      <div
        v-if="hasErrors"
        class="bg-red-lightest border border-red-light text-red-dark px-4 py-3 rounded relative mb-3"
        role="alert"
      >
        <span class="block sm:inline">Incorrect username or password.</span>
      </div>

      <form
        @submit.prevent="login"
        class="bg-white shadow-md rounded border-indigo border-t-4 px-8 pt-6 pb-8 mb-4"
      >
        <div class="mb-4">
          <label
            class="block text-grey-darker text-sm font-bold mb-2"
            for="username"
          >
            Email
          </label>
          <input
            v-model="email"
            class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker"
            id="username"
            type="email"
            placeholder="Email"
            autofocus
          />
        </div>

        <div class="mb-6">
          <label
            class="block text-grey-darker text-sm font-bold mb-2"
            for="password"
          >
            Password
          </label>
          <input
            v-model="password"
            class="shadow appearance-none border rounded w-full py-2 px-3 text-grey-darker"
            id="password"
            type="password"
            placeholder="Password"
          />
        </div>

        <div class="flex items-center justify-between">
          <button
            class="bg-indigo hover:bg-indigo-dark w-full text-white font-bold py-2 px-4 rounded"
            type="submit"
            :disabled="this.isDisabled"
            :class="{ 'opacity-50 cursor-not-allowed': this.isDisabled }"
          >
            <i v-if="isLoading" class="fa fa-spinner fa-spin fa-fw"></i>
            Sign In
          </button>
        </div>
      </form>

      <p class="text-center text-grey text-xs">
        Source code available on
        <a
          href="https://github.com/guillaumebriday/todolist-frontend-vuejs"
          class="text-grey"
          >GitHub</a
        >.
      </p>
    </div>
  </div>
</template>

<script>
  import axios from 'axios'

  export default {
    data() {
      return {
        email: '',
        password: '',
        isLoading: false,
        hasErrors: false,
      }
    },

    computed: {
      isDisabled() {
        return this.email.length === 0 || this.password.length === 0
      },
    },

    methods: {
      login() {
        if (this.isDisabled) {
          return false
        }

        this.isLoading = true
        this.hasErrors = false

        axios
          .post('auth/login', {
            email: this.email,
            password: this.password,
          })
          .then((response) => {
            this.isLoading = false

            this.$store.dispatch('login', response.data.access_token)
          })
          .catch((error) => {
            console.log(error)

            this.isLoading = false
            this.hasErrors = true
            this.password = ''
          })
      },
    },
  }
</script>

Nothing special at this point, except that TailWind CSS has been a real revelation for me! I'll talk more about it in an upcoming article.

Tests

I've started setting up tests, but I'm still having some trouble with those in JavaScript; I need to practice.

I'm using mochajs/mocha, which has a syntax that I really like and is very easy to set up.

I put all the tests in a tests subfolder and use the .spec suffix to keep track. Feel free to adapt according to your preferences.

For now, I'm testing the Login component to verify the interaction with the Sign In button:

import { mount } from 'vue-test-utils'
import Login from '../src/js/components/Login.vue'
import expect from 'expect'

/* eslint-disable no-undef */
describe('Login', () => {
  let wrapper

  beforeEach(() => {
    wrapper = mount(Login)
  })

  it('does not contain error alert', () => {
    expect(wrapper.html()).not.toContain('Incorrect username or password.')
  })

  it('enables the sign in button', () => {
    wrapper.setData({
      email: '[email protected]',
      password: 'my_precious',
    })

    let button = wrapper.find('button')

    expect(button.attributes().disabled).not.toBe('disabled')
  })

  it('disables the sign in button', () => {
    let button = wrapper.find('button')

    expect(button.attributes().disabled).toBe('disabled')
  })
})

Of course, I'll need to complete the setup with a mock of the back-end call to verify the receipt of the JWT and the redirection to the Home page.

We can also add a script in our package.json:

"scripts": {
  "test": "mocha-webpack --webpack-config webpack.config.js --require tests/setup.js tests/*.spec.js"
},

Conclusion

I can now start working on the core of the application, namely task management. You can find the code directly at guillaumebriday/todolist-frontend-vuejs.

If you have any suggestions, questions, or comments, don't hesitate.

Thank you!

Simplify your time tracking with Timecop

Timecop is a time tracking app that brings simplicity in your day to day life.

Timecop projects