And the Firebase goes on!

In the first installment of this series, I covered how fast you can kick off your dev life cycle by using the Mirage.js library as a local, and client-side, backend with a REST API in-memory service for your app.

In my second, and most recent, [installment] (https://dev.to/thisdotmedia/vue-the-mirage-from-this-angle-352p), I replaced Mirage.js with Cloud Firestore, a serverless database service offered by Firebase. This, of course, makes our app function in a more realistic manner, giving us an overview of it.

In this article, I will introduce Firebase User Authentication module to authenticate and authorize users accessing the Notes Writer app.

Since my last article, I’ve done a complete face-lift for the Notes Writer app in preparation for user authentication. The app is now more responsive and mobile friendly.

Here is a quick preview of the completed app after the addition of the User Authentication Module.

Alt Text

The login page, needless to say, is self explanatory.

Alt Text

The home page lists the existing notes, allows deleting or editing those notes and the ability to create new ones. This page introduces the Logout button as well.

Alt Text

The app is shown on a mobile. You review your existing notes by clicking the burger button located on the left side of the app header bar.

The source code of this article can be cloned from this GitHub repo: Notes Writer.

Firebase Authentication

Firebase Authentication Engine utilizes several methods to authenticate a user on your app via means such as Email with a Password, Phone, Facebook, Twitter, Apple ID, and many other options as shown here.

Alt Text

For this article, I will be using the Email/Password provider to authenticate users on the Notes Writer app.

You can always consult with the amazing Firebase Authentication docs here.

The Firebase Authentication module is also capable of doing user authorization by allowing administrators to define rules on how users can read, write, delete and edit data stored in the Cloud Firestore. You can read more about securing data in the Cloud Firestore here.

Let’s move on, and start coding user authentication in our app!

Demo

We will be adding the authentication feature on top of the new UI branch of the Notes Writer app. Therefore, start by cloning the Git branch by running this command:

git clone --branch new-ui git@github.com:bhaidar/notes-writer.git

The command clones the remote new-ui branch by creating a local copy of the branch on your computer.

Install Vue Router

Start by installing the Vue Router NPM package by running this command:

npm install vue-router

This command adds the Vue Router library into the app to allow navigation from one route to another.

Locate the /src/views folder, and add a new file named Login.vue. Initialize an empty Vue component by pasting the following content into the new file:

<template>
</template>
<script>
</script>

Save the file and switch back to the router/index.js file.

Configure Vue Router and routes

Now that the Vue Router is installed, and the Login view is created, let’s configure the Vue Router in the app, and define the different routes available for the Notes Writer app.

Inside the router/index.js file, start by adding the following imports:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from './../views/Home'
import Login from './../views/Login'
const fb = require('./../../firebaseConfig.js')

The firebaseConfig.js file is imported into this file, and is needed later on to check whether the current user is signed-in already.

Configure the Vue object to use the Vue Router plugin by adding this line of code:

Vue.use(VueRouter)

This step is required by the Vue engine to install the Vue Router plugin, and make its functionality available to the rest of the app.

Next, we need to create a new instance of the VuewRouter object, and define our routes as follows:

export const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes: [
    {
      path: '*',
      redirect: '/'
    },
    {
      path: '/',
      name: 'home',
      component: Home,
      meta: {
        requiresAuth: true
      }
    },
    {
      path: '/login',
      name: 'login',
      component: Login
    }
  ]
})

The VueRouter instance defines a catch-all route to redirect users to the Home view. The Home route defines a meta object with a single property of requiresAuth. This boolean value is used later to decide whether the route requires the user to be signed-in before accessing it.

Finally, the Login route is defined to load the Login view component.

If you are new to Vue Router plugin I suggest you visit their docs website

The Vue Router defines navigation guards and hooks. These guard and hooks are extension points you can implement to change the default behavior of the Vue Router engine when handling a specific route.

In our case, we want to implement the beforeEach navigation guard to decide whether the user can access the route he/she intends to visit. The decision is based solely on whether the route at hand requires the user to be authenticated, and that the user is indeed authenticated, and signed-in to the app.

router.beforeEach((to, from, next) => {
  const requiresAuth = to.matched.some(x => x.meta.requiresAuth)
  const currentUser = fb.auth.currentUser

  if (requiresAuth && !currentUser) next({ path: '/login', query: { redirect: to.fullPath } })
  else if (!requiresAuth && currentUser) next('/')
  else if (!requiresAuth && !currentUser) next()
  else next()
})

This hook or navigation guard accepts a single callback function that defines three main input parameters:

  • to: The route you are navigating to.
  • from: The route you are coming from.
  • next: Is a function that is used to move forward to the next hook in the pipeline, to redirect to a new route, to throw an error, and to terminate the current navigation.

The callback function above started by checking whether the route the user is navigating to requires authentication. It then uses the Firebase Auth API to get a hand on the currently signed-in user.

You can read more about managing users in Firebase Auth API here.

If the route requires authentication, and the user is not signed-in, then redirect the user to the Login view to enter their credentials, and sign-in to the app before visiting the route.

If the route doesn’t require authentication, and the user is currently signed-in, then redirect the user to the Home view.

If the route doesn’t require authentication, and the user is not signed-in, then let the user continue with the current navigation. For instance, this applies to a user visiting the Login view, or Register view.

Finally, if none of the above applies, the user is allowed to route to the desired route. This applies to the Home view. This route requires authentication, and if the user is currently signed-in, then the user is automatically redirected to the Home view.

To try this navigation guard, run the app by issuing the following command, and try to navigate to the root page /:

npm run serve

You are instantly redirected to the login page to sign-in, and access the app.

The final step is to tell the Vue about the routes that we defined above. Locate, and open the main.js file, and make sure to pass over the routes object to the root Vue instance as follows:

import Vue from 'vue'
import App from './App.vue'
import { store } from './store'
import { router } from './router'
const fb = require('./../firebaseConfig.js')

Vue.config.productionTip = false

let app
fb.auth.onAuthStateChanged(user => {
  if (!app) {
    app = new Vue({
      store,
      router,
      render: h => h(App)
    }).$mount('# app')
  }
})

Also notice that I’ve moved the initialization of the Vue root app inside the Firebase onAuthStateChanged event handler. This event is usually fired when a change occurs on the currently signed-in user. The user has signed-in, or the user has signed-out. This way, the app won’t initialize before Firebase is fully initialized.

Implement Login view

Naturally, the next step is to implement the Login view so that users can start accessing the app.

Paste the following inside the Login.vue view component:

<template>
  <div class="login">
    <section class="session">
      <div class="left"></div>
      <section>
        <header>
          <h1>Notes Writer</h1>
          <p>Welcome to Notes Writer App! Login to your account to manage your notes:</p>
        </header>
        <form
          class="form"
          @submit.prevent
        >
          <div class="form__field">
            <input
              id="email"
              type="text"
              placeholder="Email"
              autocomplete="off"
              v-model.trim="loginForm.email"
            >
            <label for="email">Email:</label>
          </div>
          <div class="form__field">
            <input
              id="password"
              type="password"
              placeholder="Password"
              autocomplete="off"
              v-model.trim="loginForm.password"
            >
            <label for="password">Password:</label>
          </div>
          <button
            @click="login"
            class="btn form__btn--submit"
          >Log In</button>
          <p
            class="errors"
            :style="{visibility: showErrors ? 'visible' : 'hidden'}"
          >Wrong username or password! Try again.</p>
        </form>
      </section>
    </section>
  </div>
</template>

<script>
import { mapMutations } from 'vuex'
const fb = require('./../../firebaseConfig.js')

export default {
  data () {
    return {
      loginForm: {
        email: '',
        password: ''
      },
      errors: null
    }
  },
  computed: {
    showErrors () {
      return this.errors
    }
  },
  methods: {
    ...mapMutations(['setCurrentUser']),
    login: async function () {
      try {
        const user = await fb.auth.signInWithEmailAndPassword(this.loginForm.email, this.loginForm.password)
        this.setCurrentUser(user.user)
        this.$router.push('/')
      } catch (error) {
        this.errors = error
      }
    }
  }
}
</script>

<style lang="scss" scoped>
@import "@/styles/components/login.scss";
</style>

The component defines a basic Login form with two fields: Email and Password.

Upon clicking the Log In button, the event handler fires:

login: async function () {
      try {
        const user = await fb.auth.signInWithEmailAndPassword(this.loginForm.email, this.loginForm.password)
        this.setCurrentUser(user.user)
        this.$router.push('/')
      } catch (error) {
        this.errors = error
      }
    }

The login() function makes use of the Firebase Auth API to sign-in the user using the signInWithEmailAndPassword() function. This function accepts as input both the user’s email and password.

If the request is successful, this function returns the currently signed-in user. The code then calls a Vuex Store mutation to store the current signed-in user. In addition, the user is redirected to the home page to start managing the notes.

In the case of an error, the code catches it, and informs the user that their sign in attempt failed.

The above introduces a new piece of state that we need to manage inside the Vuex Store instance: the currentUser. Let’s build it.

Navigate to the store/index.js file, and add the following to the state object:

state: {
    notesList: [],
    note: {},
...
currentUser: {},
  },

Also, add the mutation function as:

setCurrentUser (state, user) {
state.currentUser = user
},

Now the store is ready!

Before you can sign-in using the above Login view, we need to set the Firebase sign-in provider on this app.

Navigate to the Firebase Console.

  1. Click on your app.
  2. Click the Authentication menu item.
  3. Select the Sign-in method tab.
  4. Hover over the first row labeled Email/Password and click the pencil icon to enable this option.

Alt Text

Once you enable this provider, make sure to hit the Save button.

Next, you need to create a new user on the Firebase Console to test the app with.

  1. Select the Users tab.
  2. Click the Add user button.

Alt Text

Enter a valid email, a strong password and hit the Add user button to save the new user credentials.

Now that the backend Firebase is ready, let’s run the app, and test the work that has been done so far!

To run the app, issue the following command:

npm run serve

You enter your newly created user credentials and the app should sign you in to start managing your notes.

Add Author ID on Note model

Now that you can sign-in to the app, it’s time to introduce the field Author ID onto the Note model object. Every time you create a new Note, the app will grab the ID of the currently signed-in user and attach it on the Note model object to be stored in the Database.

Every Note should have an author or owner! This change is minor and affects only the saveNo0te() action inside the Vuex Store instance. Navigate to the /store/index.js file and amend the saveNote() action as follows:

async saveNote ({ commit, state }) {
      const { id, body, title } = state.note
      const authorId = state.currentUser.uid

      if (id) { // update
        commit('setPerformingUpdate', true)
        await fb.notesCollection.doc(id).update({
          body,
          title,
          updatedOn: fb.firebase.firestore.Timestamp.now()
        })
        commit('setPerformingUpdate', !state.performingUpdate)
      } else { // add
        commit('setPerformingAdd', true)
        await fb.notesCollection.add({
          body,
          title,
          **authorId,**
          createdOn: fb.firebase.firestore.Timestamp.now(),
          updatedOn: fb.firebase.firestore.Timestamp.now()
        })
        commit('setPerformingAdd', !state.performingAdd)
      }
      commit('setNote', {})
    }

When creating a new Note record, the code retrieves the currently signed-in User ID and stores this value inside a local variable named authorId. This variable is then passed over to the notesCollection.add() function when creating a new Note record as I’ve just shown in the source code.

That’s all! Now every Note created in the system has an owner or author. You will see shortly how we are going to use this feature to query for customized and owned Notes only.

Integrate Authentication into the Vuex Store

The Vuex Store instance should be updated whenever the status of the currently signed-in user changes. To cater for that, we will refactor the code inside the store object as follows:

fb.auth.onAuthStateChanged(user => {
  if (user) {
    store.commit('setCurrentUser', user)

    // realtime updates from our notes collection
    fb.notesCollection.orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {
      let notesArray = []

      querySnapshot.forEach(doc => {
        let note = doc.data()
        note.id = doc.id
        notesArray.push(note)
      })

      store.commit('loadNotes', notesArray)
    })
  }
})

Now the store watches real-time changes on the notesCollection inside the onAuthStatechanged event handler callback.

If there is a valid user passed over, the store is updated accordingly. Then the store starts watching any changes on the notesCollection object.

Query for User’s own Notes only

So far the Vuex Store instance is watching the entire notesCollection object. However, what’s needed is to query only the notes that belong to the currently signed-in user. To achieve this goal, navigate to the store/index.js file, and replace the following line of code:

fb.auth.onAuthStateChanged(user => {
  if (user) {
    store.commit('setCurrentUser', user)

    // real-time updates from our notes collection
    **fb.notesCollection.orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {**
      let notesArray = []

      querySnapshot.forEach(doc => {
        let note = doc.data()
        note.id = doc.id
        notesArray.push(note)
      })

      store.commit('loadNotes', notesArray)
    })
  }
})

With the following line of code:

fb.auth.onAuthStateChanged(user => {
  if (user) {
    store.commit('setCurrentUser', user)

    // real-time updates from our notes collection
    fb.notesCollection.where('authorId', '==', user.uid).orderBy('createdOn', 'desc').onSnapshot(querySnapshot => {
      let notesArray = []

      querySnapshot.forEach(doc => {
        let note = doc.data()
        note.id = doc.id
        notesArray.push(note)
      })

      store.commit('loadNotes', notesArray)
    })
  }
})

The code now fetches Note records that belong to the currently signed-in user!

Configure Authorization rules on Firebase

Previously in this series, I created the Database and opted for the Start in test mode. This mode allows anyone to read and write to the database for 30 days.

Now that we have authentication in place, let’s reassess the Cloud Firestore rules and allow only authenticated users to read, update, create and delete.

Follow the steps below to setup authorization on your Cloud Firestore:

  1. Visit the Firebase console and login to your account.
  2. Locate and click the Database menu item..
  3. Click the Rules tab.

Replace the content there with:

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, create, update, delete: if request.auth.uid != null
    }
  }
}

This rule targets any document in the current database and allows any requests that hold an auth object with an uid that’s not null. Only requests sent by an authenticated user, will carry a valid auth object.

That’s it! Pretty easy. Now we can be sure that anonymous requests won’t find their way into our database.

Add Logout button

Finally, let’s add support for a Logout button for the user.

Navigate to the components/Navbar.vue component, and insert the following inside the links section:

<div class="links">
      <a
        href="# "
        title="Logout"
        v-if="showLogout"
        @click="logout"
      ><span class="logout">Logout</span></a>
    </div>

Let’s implement the logout() function as follows:

async logout () {
      await fb.auth.signOut()
      this.clearData()
      this.$router.push('/login')
    },

The code calls the signOut() function on the Firebase Auth API to sign-out the user, and clear any local cookies or data related to the currently signed-in user.

In addition, it clears the data stored about the currently signed-in user inside the Vuex Store instance.

Finally, it redirects the user to the Login page.

Let’s add the clearData() action onto the Vuex Store instance. Navigate to the store/index.js file, and add the following action:

clearData ({ commit }) {
      commit('setCurrentUser', {})
      commit('loadNotes', [])
      commit('setNote', {})
    },

The action clears out the signed-in user, all loaded Notes records, and the current Note object.

Conclusion

We’re done! We’ve implemented user authentication in the Notes Writer app using Firebase Authentication API. The Firebase Authentication is rich in features, and offers more functionalities than we could possibly cover in this article. My suggestion: always refer to the Firebase Authentication docs website to learn more about the different options available.

In the next installment, we’ll make use of Firebase Cloud Functions to both extend the features of the Notes Writer app, and demonstrate the capabilities of the Cloud Functions.

Stay tuned!

This Dot Inc. is a consulting company which contains two branches : the media stream, and labs stream. This Dot Media is the portion responsible for keeping developers up to date with advancements in the web platform. This Dot Labs provides teams with web platform expertise, using methods such as mentoring and training.

This post is also available on DEV.