Basic Single Page application using Vue.js and Firebase — Part 2

In part 1 we’ve created a basic structure for our app. In this part we will add a Firebase and implement an authorization functionality.

In this tutorial I want to focus on email/password method of authentication. This is a common way to add authorization to app. Still, with Firebase you can set up several OAuth providers like Google, Facebook and Twitter.

If you would like not to start this part from scratch you can clone previous part with:

$ git clone -b part-1 https://github.com/oleg-agapov/basic-spa-vue-firebase demo-app

Firebase

Let’s start with creating Firebase account here. Go to Console and click “Add project”. Give it any name you like and choose your country (country doesn’t affect the project). After that you will be redirected to home page of a newly created project. From this console you can configure your Firebase app.

In order to use Firebase in our app we need to setup Firebase SDK first. To do that just run following (don’t forget to stop dev server if it’s up):

$ npm install --save firebase

Run development server again. Now we need to import Firebase. Open src/main.js and add following:

import firebase from 'firebase'
import { firebaseConfig } from './config'
firebase.initializeApp(firebaseConfig)

Here in first row we import Firebase SDK. In second row we import configuration object. We will create it soon. And finally we call initializeApp() method on firebase class in order to pass our configuration to SDK. Full listing of src/main.js :

Don’t worry if your server throw an error. It’s because we need to create a config file. In src/ create a file called config.js and add following:

export const firebaseConfig = {
apiKey: 'YOUR_API_KEY',
authDomain: 'YOUR_AUTH_DOMAIN',
databaseURL: 'YOUR_DATABASE_URL',
projectId: 'YOUR_PROJECT_ID'
}

You need to replace values with yours. Configuration object for your application you can find in Firebase console. Go to Authentication, in top right corner you will see “WEB SETUP” button. Click on it and you will get your credentials.

Also while you are in Authentication go to Sign-In Methods tab. There you need to enable Email/Password provider. Now we are fully ready to start implementing sign-in/sign-up functionality.

Note: I highly recommend to install extension for Chrome/Firefox called Vue.js devtools. This extension will help you in debugging your Vue.js application and to see what is happening in Vuex store.

Prepare Vuex store for authorization functionality

Before introducing sign-in/up flow I would like to setup some helper functions and states in Vuex store.

First of all, in state.js add following states:

export const state = {
appTitle: 'My Awesome App',
user: null,
error: null,
loading: false
}

Here I’ve added three objects. user object will contain authorized user’s data, error for storing errors, loading flag will indicate if application is still loading any data. On initialization we don’t have user data and errors so they are null, loading state is false.

To get those values in components we need to setup getters. In src/getters.js add following:

These are simple getters to extract user, error and loading data from store.

And finally let’s create mutations to change the state values (src/mutations.js):

Great! We now have initial state along with setters and getters for it. All preparations are done now. Let’s proceed with sign up.

Sign up

Inside Signup.vue component we have pretty much complete visual part of a sign up process. Let’s think of a functional part.

First of all we need to connect our text fields to data. We can do that by adding v-model attribute to text fields.

<v-flex>
<v-text-field
name="email"
label="Email"
id="email"
type="email"
v-model="email"
required></v-text-field>
</v-flex>
<v-flex>
<v-text-field
name="password"
label="Password"
id="password"
type="password"
v-model="password"
required></v-text-field>
</v-flex>
<v-flex>
<v-text-field
name="confirmPassword"
label="Confirm Password"
id="confirmPassword"
type="password"
v-model="passwordConfirm"
></v-text-field>
</v-flex>

Next thing is to initialize our variables in data property:

export default {
data () {
return {
email: '',
password: '',
passwordConfirm: ''
}
}
}

Now our text fields are binded to variables.

As you can see we have “Confirm Password” field which should show an error to user if the passwords don’t match. We can add this check easily. Add new computed property:

computed: {
comparePasswords () {
return this.password === this.passwordConfirm ? true : 'Passwords don\'t match'
}
}

This function do a check if Password field value is equal to Confirm Password. If they are equal it returns true, if not — string “Passwords don’t match”. Pay attention that I can access data property inside computed property through this context (so I write this.password and this.passwordConfirm).

To apply this check I can use built-in rules attribute for confirmPassword text field like this:

<v-text-field
name="confirmPassword"
label="Confirm Password"
id="confirmPassword"
type="password"
v-model="passwordConfirm"
:rules="[comparePasswords]"
></v-text-field>

This check runs continuously, so if you type at least one character to Password field this check will throw an error. But once you type the same password in Confirm Password field it will disappear.

To submit email and password let’s add submit listener to our form like this:

<form @submit.prevent="userSignUp">

Here @ is equivalent to v-on — Vue.js event listener. We want to listen submit event on form. Modification .prevent is a shortcut for event.preventDefault() so no need to add this to our userSignUp method (this method prevents default behavior of forms e.g. do a POST request and reload the page). Submit event will trigger userSignUp function. Let’s add it as a method:

methods: {
userSignUp () {
if (this.comparePasswords !== true) {
return
}
this.$store.dispatch('userSignUp', { email: this.email, password: this.password })
}
}

In this method we first check if passwords are equal. If they don’t — just stop execution of the function by calling return. Otherwise we dispatch an action userSignUp from our store and pass email and password as a parameters. Pay attention here how I call the store. Because I registered it in src/main.js file I can access to store in any component by calling this.$store.

If you got stuck you can refer to full listing of Signup.vue in the end of this chapter.

Time to add an action to handle sign up data and pass it to Firebase. Open src/store/actions.js and add:

import firebase from 'firebase'
import router from '@/router'
export const actions = {
userSignUp ({commit}, payload) {
commit('setLoading', true)
firebase.auth().createUserWithEmailAndPassword(payload.email, payload.password)
.then(firebaseUser => {
commit('setUser', firebaseUser)
commit('setLoading', false)
router.push('/home')
})
.catch(error => {
commit('setError', error.message)
commit('setLoading', false)
})
}
}

Whoa! A lot of code, but let’s look at it step-by-step. First of all we need to import firebase. We will handle authorization process from our store actions. Then we need to import router in order to redirect our user to /home page in case of successful sign up. In actions object we have userSignUp function which take two arguments: commits to have access to Vuex mutations and payload to receive data from components (in our case email and password).

Inside userSignUp we first set our loading state to true. Later you will see how to use loading in our app. Next we call two methods on firebase (a global namespace from which all the Firebase services are accessed). First method calls auth() service. Second method is createUserWithEmailAndPassword(email, password) and it will do all the magic to create a new user. We pass payload.email and payload.password as a parameters. This method will return a Promise which will contain a data regarding newly created user. All I do is just call mutation ‘setUser’ and pass firebaseUser to it in line

commit(‘setUser’, firebaseUser)

Then we need to set loading back to false (because we received the results) and redirect user to /home page using router.push(). In case of error we have a catch block which will commit our error message and set loading to false.

In theory we are able to create a new users with email and password:) Let’s check it. Navigate to /signup page and do a registration.

Note. At this stage you can register with any fake email in format user@domain.com and a password that is longer than 6 symbols.

If everything is fine you should be redirected to /home page. Now you are able to see new user’s email in Firebase console → Authentication. Also you can check what is the value of user state. You can do it in Vue.js devtools.

And finally we need to close our feedback loop by handling our error and loading states. First we need to pull them from the store. We can do this in computed property:

computed: {
comparePasswords () {
...
}
error () {
return this.$store.getters.getError
},
loading () {
return this.$store.getters.getLoading
}
}

Loading state we can apply to Sign Up button like this:

<v-btn primary type="submit" :disabled="loading">Sign Up</v-btn>

Because response from Firebase will take some time we just disable submit button (to prevent double-submit) until the request is in progress.

Now we need to show error message to user. I will use Vuetify v-alert component for this. In Signup.vue right after opening form tag add:

<v-flex>
<v-alert error dismissible v-model="alert">
{{ error }}
</v-alert>
</v-flex>

This alert is controlled by boolean alert variable. Attribute dismissible allows to close this alert, error attribute will add red color style. Inside v-alert we render our error message in curly braces. This alert should works like this:

  1. Initially alert variable is false meaning that alert is closed. error is null (as it’s an initial state)
  2. We need to listen to error and as soon as we have not null value we need to set alert to true (to trigger v-alert)
  3. When user dismiss the alert, then corresponding alert variable will be set to false. So we need to listen to alert variable and as soon it is a false we need to reset our error back to null

In order to implement this we can use watch property. Also not forget to add alert to data property. Here is what you need to add to Signup.vue:

data () {
...,
alert: false
},
...
watch: {
error (value) {
if (value) {
this.alert = true
}
},
alert (value) {
if (!value) {
this.$store.dispatch('setError', null)
}
}
}

And one more change we need to add to our actions.js. In successful block add following code after committing loading to false:

commit('setError', null)

This will cover a case when user had an error but didn’t dismiss the error. If later he will successfully signed up with correct credential, we will erase the error from actions.

Here is a full listing of Signup.vue:

Sign in

Now it’s turn to implement sign in flow. Basically, it’s very similar to sign up: we need to get user’s credentials and pass them to Firebase, get response, handle errors if any. I’ll just give you a full Signin.vue component and comment it:

First thing is to add is v-model’s for text fields. Don’t forget to add corresponding email and password variables to data property.

Then add @submit.prevent=”userSignIn” event listener to form tag. Next is to create a method userSignIn. It will just dispatch userSignIn action from Vuex store.

Add alert variable to data property with initial state as false. Add error and loading to computed properties and return corresponding values from store. In watch property set a watchers to handle alert with error. Finally add v-alert component to the top of form tag.

Only one thing is left — to create userSignIn action in src/store/actions.js (add it right after sign up function):

userSignIn ({commit}, payload) {
commit('setLoading', true)
firebase.auth().signInWithEmailAndPassword(payload.email, payload.password)
.then(firebaseUser => {
commit('setUser', firebaseUser)
commit('setLoading', false)
commit('setError', null)
router.push('/home')
})
.catch(error => {
commit('setError', error.message)
commit('setLoading', false)
})
}

As you can see it’s very similar to userSignIn action. Difference is only in Firebase method I call to sign in my user — signInWithEmailAndPassword.

Here is the source code of actions.js so far:

To test sign in you can do next. Reload the page (with F5 on Windows or Cmd+R on macOs) and open Vuex in Vue.js devtools. You should see that your user object is null. If that is true, go to /signin and try to login with credentials you used for sign up. If everything is works fine you should be able to sign in successfully and user object in store should contain user’s data.

Authorization check and log out

While we did sign in test we did reload the page. Indeed, reloading resets your Vuex state to initial value. In real life it’s not a case, such behavior is not suitable for users. Because every time user reload the page he will need to sign in again.

Firebase has a solution for that case. There is a special observer called onAuthStateChanged which will help your app to figure out whether your user is currently signed in or not.

Intuition behind this observer is next. Open developer console in Chrome and go to Application → Storage → Local Storage (for Firefox you need to install extension to be able to see Local Storage). Here on the right side you should see a key which starts with firebase:authUser:. This variable stores credential to your signed in user account. When you put onAuthStateChanged somewhere in your code this observer will look after this key. If the key is present — observer will return you a user, if not — null.

Let’s use it! In src/main.js add following to created() lifecycle hook of Vue instance:

new Vue({
el: '#app',
router,
store,
render: h => h(App),
created () {
firebase.auth().onAuthStateChanged((firebaseUser) => {
if (firebaseUser) {
store.dispatch('autoSignIn', firebaseUser)
}
})
}
})

As I said earlier onAuthStateChanged observer will return to us Firebase user as soon as it recognize that user is already signed in. If firebaseUser is present in a callback we just dispatch autoSignIn action. This action will set our user to firebaseUser. Let’s add this action:

autoSignIn ({commit}, payload) {
commit('setUser', payload)
}

As I said this action simply set our user state.

You can ask me: why do we need to create a separate action to call only one mutation, why just not use this mutation directly in main.js like commit(‘setUser’, firebaseUser)? Good question. Actually you can. But there are several gotchas on this. First, it’s kind of anti-pattern to call mutations outside actions. Second you will lose time-travel option in debugger. And lastly with time you would want to add some additional code to execute during autoSignIn action, so it will be easier to do.

Now you can try to reload the page and see that our user state in store is populated automatically.

Time to think on two other features. First of all let’s add a “Sign Out” button. Second, our menu in toolbar show the same buttons regardless user authentication state. It would be good to show “Sign In” and “Sign Up” buttons to unauthorized users, “Home” and “Sign Out” for authorized.

To add “Sign Out” button open src/App.vue component. Because log out doesn’t have a separate page view (like /signin or /signup) we will not add this button to menuItems array (which contain all other buttons). We will just add a new item outside v-for directive. Here is what I mean:

...
<v-navigation-drawer temporary v-model="sidebar">
<v-list>
<v-list-tile
v-for="item in menuItems"
:key="item.title"
:to="item.path">
<v-list-tile-action>
<v-icon>{{ item.icon }}</v-icon>
</v-list-tile-action>
<v-list-tile-content>{{ item.title }}</v-list-tile-content>
</v-list-tile>
<v-list-tile @click="userSignOut">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Sign Out</v-list-tile-content>
</v-list-tile>

</v-list>
</v-navigation-drawer>
...
<v-toolbar-items class="hidden-xs-only">
<v-btn
flat
v-for="item in menuItems"
:key="item.title"
:to="item.path">
<v-icon left dark>{{ item.icon }}</v-icon>
{{ item.title }}
</v-btn>
<v-btn flat @click="userSignOut">
<v-icon left>exit_to_app</v-icon>
Sign Out
</v-btn>

</v-toolbar-items>

So, in navigation drawer we have a list, inside list there is v-list-tile tag which has v-for directive. Below it create a new list tile with click listener @click=”userSignOut”. And very similar in v-toolbar-items. Here we iterate v-btn ‘s, so add a new v-btn after it with the same click listener. Now you should see “Sign Out” button in menu.

To make it work let’s create a userSignOut method (in src/App.vue):

methods: {
userSignOut () {
this.$store.dispatch('userSignOut')
}
}

This method will simply dispatch userSignOut action from our store. So in src/store/actions.js add:

userSignOut ({commit}) {
firebase.auth().signOut()
commit('setUser', null)
router.push('/')
}

Here we first signing user out from firebase, then reset our user in store and finally redirecting to /. Now you can sign out from application.

Let’s improve user experience by hiding unnecessary buttons from authorized users. In App.vue add new computed property:

isAuthenticated () {
return this.$store.getters.getUser !== null && this.$store.getters.getUser !== undefined
}

This function will return authentication state. Depends on this function we now can render only necessary menu items for user. Remove menuItems from data property and add new computed property:

menuItems () {
if (this.isAuthenticated) {
return [
{ title: 'Home', path: '/home', icon: 'home' }
]
} else {
return [
{ title: 'Sign Up', path: '/signup', icon: 'face' },
{ title: 'Sign In', path: '/signin', icon: 'lock_open' }
]
}
}

So, if user is authenticated then menuItems will return only “Home” link, otherwise “Sign Up” and “Sign In”.

Great! Only one side effect is our “Sign Out” link that is outside our menuItems. That is why we need to add isAuthenticated check to sign out buttons with v-if like this:

...
<v-list-tile v-if="isAuthenticated" @click="userSignOut">
<v-list-tile-action>
<v-icon>exit_to_app</v-icon>
</v-list-tile-action>
<v-list-tile-content>Sign Out</v-list-tile-content>
</v-list-tile>
...
<v-btn flat v-if="isAuthenticated" @click="userSignOut">
<v-icon left dark>exit_to_app</v-icon>
Sign Out
</v-btn>
...

Check menu links once again. Now they should reflect user’s authentication state.

Protected routes

One of the main purpose of creating a users is having secret routes which are accessible only for authorized members. Let’s do a quick demonstration. Sign out from application if use are in. Now enter in your browser address bar localhost:8080/home. As you can see you successfully entered /home route despite it should be closed (by logic) for non-authorized users.

To avoid such behavior in Vue-router library exists special functions to guard navigations either by redirecting it or canceling it. Let’s protect /home route with navigation guard.

Open src/router/index.js. First thing first, change /home path option in routerOptions:

{ path: '/home', component: 'Home', meta: { requiresAuth: true } }

Here we use meta route field. It’s a special field which we will use a bit late. For now only option we have inside meta field is requiresAuth: true. This option we will use to identify protected routes. Now add meta to routes:

const routes = routerOptions.map(route => {
return {
path: route.path,
component: () => import(`@/components/${route.component}.vue`),
meta: route.meta
}
})

To enable navigation protection we will use global guard named beforeEach. This is a function applied to router instance which will be executed each time you enter new route. Let’s do some refactoring of router file:

Changes I’ve added:

  • instead of export default new Router({..}) I’ve created new constant router in order to refer it in next statement
  • beforeEach function. This guard function have three arguments: to — target route object, from — current route being navigated away from, next — a function that must be called in order to resolve the next action. Inside it we have two constants. First constant will return true if route you are intend to navigate is protected. Second constant will return firebase current user (of course if user is authenticated). In order to use this function you need to import firebase SDK at the top. And finally we do a check: if route requires auth and user is not authenticated we just call next function with /signin argument in order to redirect to sign in page. Otherwise we just call next() with no argument to proceed navigation.
  • in the end just export router as default

Now you can check protected route. Do a sign out first. Then try to open localhost:8080/home and you should be redirected to /signin.

Great!

Not really :)

Now sign in to application. You will be redirected to /home. Now reload the page. Wat? Why you appeared on /signin? Because when beforeEach do a check it has firebase.auth().currentUser is null. This happens because during execution of beforeEach function our Firebase onAuthStateChanged didn’t resolved user status yet.

Solution: run beforeEach function after onAuthStateChanged will resolve user status. Here is how you can do it (in src/main.js):

/* eslint-disable no-new */
const unsubscribe = firebase.auth()
.onAuthStateChanged((firebaseUser) => {
new Vue({
el: '#app',
router,
store,
render: h => h(App),
created () {
store.dispatch('autoSignIn', firebaseUser)
}
})
unsubscribe()
})

This code is explained here, here and here. Long story short, we call our Vue instance after observer onAuthStateChanged finish a check. And after resolving user’s state we stop the observer by calling unsubscribe().

Now we should have expected behavior of the protected routes.

Catch-all route

Despite this topic not related to authentication system I want to have it since we are adjusting our router’s work. Here I want to ask a question: what if user will enter in address bar something like localhost:8080/ksjhfkdsjhfsdjgh, so absolutely meaningless path? Well, he will see an empty screen without content. That is why it’s a good idea to setup a catch-all route which will render some error page to such routes.

First of all let’s create a new component called src/components/NotFound.vue:

So it is a simple error page which says that user entered incorrect link.

To setup catch-all route we need to slightly change our router:

import NotFound from '@/components/NotFound'
const router = new Router({
mode: 'history',
routes: [
...routes,
{ path: '*', component: NotFound }
]

})

Here somewhere in the top you need to import NotFound component. Then inside our router configuration object I use spread syntax to inherit my routes array and add new entry with path: ‘*’ which will render component NotFound. Path ‘*’ is a wildcard so it is any route except existing ones. Now if you try to enter incorrect route you will see a message saying that you have a broken link.

Summary

Wow, it was a long article. Despite we used only a few Firebase functions we’ve added a lot of functionality to use them. Let me know in comments if anything is wrong.

In part 3 we will discuss a database: standard CRUD operations, security rules and some thought on how we can organize our database schema.

Code for this part is here: https://github.com/oleg-agapov/basic-spa-vue-firebase/tree/part-2

Cheers!