How To Build A Vue Survey App Using Firebase Authentication And Database
How To Build A Vue Survey App Using Firebase Authentication And Database
David Atanda
In this tutorial, you’ll be building a Survey App, where we’ll learn to validate our users form data, implement Authentication in Vue, and be able to receive survey data using Vue and Firebase (a BaaS platform).
As we build this app, we’ll be learning how to handle form validation for different kinds of data, including reaching out to the backend to check if an email is already taken, even before the user submits the form during sign up.
Also, the app would handle logging in of the user with restful APIs. It’ll make use of Authguard in Vue router to prevent users that are not logged in from getting access to the survey form, and successfully send the survey data of logged-in users to a secure database.
Just so we’re on the same page, let’s clarify what Firebase is, and what it’ll be doing in this tutorial. Firebase is a toolset to “build, improve, and grow your app”, it gives you access to a large portion of the services that developers would normally have to build themselves, but don’t really want to build, because they’d rather be focusing on the app experience itself. This includes things like analytics, authentication, databases, file storage, and the list goes on.
This is different than traditional app development, which typically involves writing both frontend and backend software. The frontend code just invokes API endpoints exposed by the backend, and the backend code actually does the work. However, with Firebase products, the traditional backend is bypassed, putting the work into the client. This technically allows front-end engineers like myself to build full-stack applications writing just front end code.
The bottom line is that Firebase would act as our backend in this project by providing us with the necessary API endpoints to handle both our authentication and database needs. In the end, you’ll have built a functional survey app using Vue+ Firebase. After that, you can go ahead and build any web app of your choice using these same processes, even with a custom backend.
To follow along, you need to have Node and npm/yarn installed on your machine. If you do not have that done already, follow these quick guides to install yarn or npm on your machine. You also need to have a basic understanding of Vue, Vuex and Vue router syntax for this tutorial.
The starter files for this tutorial are right here, which contains the base files for this project, and here is the repo for the completed demo. You can clone or download the repos and run npm install
in your terminal.
After installing the starter file, you’ll see a welcome page, which has the options to sign up and sign in. After getting logged in you can then have access to the survey.
Feel free to create a new project if you’ll like to build this project entirely on your own, just make sure to install Vuex, Vue router, Vuelidate and axios into your Vue project. So let’s jump right in:
First, we’ll need a Firebase account to set up this project which is very much like creating a container for our app, giving us access to the database, various means of authentication, hosting, etc. It’s straight forward to set up once you’re on the Firebase site.
Now that we have our project, the next thing is to set up both our authentication system and database (Realtime database) on Firebase.
- Click on the “authentication” option;
- Set up the “sign-in method” we want (in this case email/password).
- Click on “database”.
- Choose “Realtime database” and copy this link that’s right on top.
It’ll be very useful as the API endpoint when we want to send the data to our firebase database.
We’ll refer to this API as the database API. To use it, you’ll have to add the name of the database of your choice when sending it. For example, to send to a database called user. You simply add user.json at the end:
{databaseAPI}/user.json
After this, we’ll then go to Firebase auth rest API documentation to get our sign up and sign in API endpoints. Within these endpoints, there will be a need for our project’s API key, which can be found in our project settings.
Validation
Back to our code, there’ll be a validation of the signup data before been sent to the server, just to make sure the user is sending appropriate information. We’ll be using Vuelidate which is a cool library that makes validation easier in Vue. First of all, install Vuelidate into the project:
npm i vuelidate
Go to src/components/auth/signup.vue
and within the script tag import vuelidate and all the necessary events that we’ll need from the library as seen below.
Note: You can check the docs for a full overview of the library and all available events.
import { required, email, numeric, minValue, minLength, sameAs } from 'vuelidate/lib/validators'
A quick explanation:
required |
The value is compulsory |
email |
Value must be an email |
numeric |
Must be a number |
minValue |
Least numerical value the user can input. |
sameAs |
Used to compare between two values to make sure they’re the same |
Also import axios
to be able to send an HTTP request to the server:
import axios from 'axios'
Before we go on, we’ll need to add some rules to the database to be able to validate the email as we should, just as seen below:
"read" = "true"
Meaning that the database can be read without any hindrance from the client-side.
"write" = "auth" !== null
You can’t write on the database except you’re an authenticated user.
"Users" = { "onIndex" : ["email"] }
This allows us to query the users
document with an index of email
. That is, you can literally filter the database for a unique email.
Then add a custom computed property with the name validations
just like we have methods, computed, etc.
Under validations
we’ll have methods to validate the necessary data starting from email
where it’s required and obviously must be an email. Also, we want to be able to tell a user when an email has already been taken by someone else, by checking the database after the user has typed it using something called async validators all within a custom validator and it’s all supported by vuelidate.
validations : { email: { required, email, unique: val => { if (val === '') return true return axios.get('https://vue-journal.firebaseio.com/users.json?orderBy="email"&equalTo="' + val + '"') .then(res => { return Object.keys(res.data).length === 0 }) } } }
Then under unique, query the database using axios and use the default Object.keys to return the response only if it’s length is 0.
For the age, you’ll add required, numeric, and a min value of 18 that’s assigned to minVal
as its properties.
age: { required, numeric, minVal: minValue(18) }
Password’s properties are required, with a minimum length of 6 assigned to minLen
.
password: { required, minLen: minLength(6) }
confirmPassword
properties are basically to be the same as the password.
confirmPassword: { sameAs: sameAs(vm => { return vm.password }) }
To tell the user that the email is taken, use v-if
to check if unique
is true or false. If true, then it means that the returned Object’s length is 0, and email can still be used as well as vice versa.
In the same manner, you can check if the user input is an actual email using v-if
.
And for all the surrounding divs on the individual input, we will add a class of invalid that becomes active once there’s an error on that input.
To bind the validation events to each of the input in the HTML, we use $ touch()
as seen with the email
below.
<div class="input" :class="{invalid: $ v.email.$ error}"> <h6 v-if="!$ v.email.email">Please provide a valid email address.</h6> <h6 v-if="!$ v.email.unique">This email address has been taken.</h6> <input type="email" placeholder="Email" id="email" @blur="$ v.email.$ touch()" v-model="email"> </div>
Age
, password
, and confirmPassword
will be binded to their HTML input in a similar manner as the email
.
And we’ll make the ‘Submit’ button inactive if there’s an error in any of the input.
<button type="submit" :disabled="$ v.$ invalid">create</button>
Here’s a complete CodePen example for this vuelidate section.
Authentication
This app is an SPA and doesn’t reload like traditional sites, so we’ll be using Vuex, as our single “source of truth” to allow every component in our app to be aware of the general authentication status. We go to our store file, and create both sign-in/sign-up method within actions.
The response (token
and userId
) received when we send the users data, are going to be stored within our state. This is important because the token is going to be used to know if we’re still logged in or not at any point within our app.
The token
, userId
, and user are created in the state with an initial value of null. We’ll get to the user much later, but for now, we’ll focus on the first two.
state: { idToken: null, userId: null, user: null }
Mutations are then created to change the state when needed.
authUser |
Saves the token and userId |
storeUser |
Stores the user info |
clearAuthData |
Erases the data back to the initial state |
mutations: { authUser (state, userData) { state.idToken = userData.token state.userId = userData.userId }, storeUser (state, user) { state.user = user }, clearAuthData (state) { state.idToken = null state.userId = null state.user = null } }
For sign-up/sign-in, we’ll have to create individual actions for both, where we send our auth requests to the server. After which our response(token and userId) from sign-up/sign-in is committed to authUser, and saved on the local storage.
signup ({commit, dispatch}, authData) { axios.post('https://www.googleapis.com/identitytoolkit/v3/relyingparty/signupNewUser?key=AIzaSyCFr-OMMzDGp4Mmr0t66w2cTGfNazYjptQ', { email: authData.email, password: authData.password, returnSecureToken: true }) .then(res => { console.log(res) commit('authUser', { token: res.data.idToken, userId: res.data.localId }) localStorage.setItem('token', res.data.idToken) localStorage.setItem('userId', res.data.localId) localStorage.setItem('email', res.data.email) dispatch('storeUser', authData) setTimeout(function () { router.push('/dashboard') }, 3000) }) .catch(error => console.log(error)) }
login ({commit}, authData) { axios.post('https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPassword?key=AIzaSyCFr-OMMzDGp4Mmr0t66w2cTGfNazYjptQ', { email: authData.email, password: authData.password, returnSecureToken: true }) .then(res => { console.log(res) localStorage.setItem('token', res.data.idToken) localStorage.setItem('userId', res.data.localId) localStorage.setItem('email', res.data.email) commit('authUser', { token: res.data.idToken, userId: res.data.localId }) router.push('/dashboard') }) .catch(error => console.log(error.message)) }
But here’s the tricky part, what we’ll do with the sign-up action particularly is to send only the email and password to be registered in the authentication database. In the real sense, we don’t have access to use the data in this authentication database, and we did not send any of our sign-up data besides email/password.
So what we’ll do is to create another action to send the complete sign-up data to another database. In this separate database document, we’ll have complete access to all the information we choose to save there. We’ll call this new action is called storeUser
We then go to our sign-up action and dispatch the entire object containing our sign-up data to a database we now have access to through storeUser
.
Note: You might not want to send your user’s password with storeUser
to the database for security reasons.
storeUser ({ state}, userData) { if (!state.idToken) { return } axios.post('https://vue-journal.firebaseio.com/users.json' + '?auth=' + state.idToken, userData) .then(res => console.log(res)) .catch(error => console.log(error)) } }
storeUser
adds a query using our newly gotten token and database API while posting to the database.
This is because we cannot write to our database, except we’re authenticated with our proof( the token). That’s the rule we gave Firebase at the beginning, remember?
“write” = “auth” !== null
The complete code for sign-up/sign-in actions are right here.
Then dispatch both the sign-up and sign-in from their components within the onSubmit
method to the respective actions in the store.
methods : { onSubmit () { const signupData = { email : this.email, name : this.name, age : this.age, password : this.password, confirmPassword : this.co nfirmPassword } this.$ store.dispatch('signup', signupData) } } }
Note: signupData
contains the form’s data.
methods : { onSubmit = { const formData = { email : this.email, password : this.password } this.$ store.dispatch('login', {email: formData.email, password: formData.password}) } }
AuthGuard
There’s a need for AuthGuard to prevent users that are not logged in from getting access to the dashboard where they’ll send the survey.
Go to the route file and import our store.
import store from './store'
Within the route, go to the dashboard’s path and add the following:
const routes = [ { path: '/', component: WelcomePage }, { path: '/signup', component: SignupPage }, { path: '/signin', component: SigninPage }, { path: '/dashboard', component: DashboardPage, beforeEnter (to, from, next) { if (store.state.idToken) { next() } else { next('/signin') } } } ]
All this does is to check whether there’s a token in the state, if yes, we give access to the dashboard and vice versa.
LogOut
To create our logout option we’ll make use of clearAuth
that we created earlier under mutations
which just sets both the token
and userId
to null
.
We now create a new logout
action
, that commits to clearAuth
, delete local storage and add router.replace('/')
to redirect the user completely.
actions: { logout ({commit}) { commit('clearAuth') localStorage.removeItem('token') localStorage.removeItem('userId') router.replace('/') } }
In the header component, we have an onLogout
method which dispatches our logout action in the store.
methods: { onLogout() { this.$ store.dispatch('logout') } }
We then add a @click
to the button which fires the onLogout
method as we can see here.
<ul @click="onLogout">Log Out</ul>
UI_State
Now that we’ve given conditional access to the dashboard, the next step is to remove it from the nav bar, so only authenticated users can view it. To do that, we would add a new method under the getters
called ifAuthenticated
which checks if the token within our state is null. When there’s a token, it shows that that the user is authenticated and we want them to see the survey dashboard option on the nav bar.
getters: { isAuthenticated (state) { return state.idToken !== null } }
After which, you go back to the header component and create a method auth
under computed, which dispatches to our isAuthenticated
within the getters
we’ve just created in the store. What this does is that isAuthenticated
would return false if there’s no token, which means auth
would also be null and vice versa.
computed: { auth () { return this.$ store.getters.ifAuthenticated } }
After this, we add a v-if
to our HTML to check if auth
is null or not, determining whether that option would show on the nav bar.
<li v-if='auth'> <router-link to="/dashboard">Dashboard</router-link> </li> <li v-if='!auth'> <router-link to="/signup">Register</router-link> </li> <li v-if='!auth'> <router-link to="/signin">Log In</router-link> </li>
- You’ll find the complete code of the UI State section here.
AutoLogin
When we reload our app we lose the data and are signed out, having to start all over. This is because our token and Id are stored in Vuex, which is javascript, and this means that our app gets reloaded with the browser when refreshed.
And so finally, what we’ll be doing is to retrieve the token within our local storage. By so doing, we can have the user’s token on the browser regardless of when we refresh the window, and have a method auto log-in our user in as much as the token is still valid.
A new actions
method called AutoLogin
is created, where we’ll get the token and userId
from the local storage, and commit our data to the authUser
method in the mutations.
actions : { AutoLogin ({commit}) { const token = localStorage.getItem('token') if (!token) { return } const userId = localStorage.getItem('userId') const token = localStorage.getItem('token') commit('authUser', { idToken: token, userId: userId }) } }
We then go to our App.vue and write a created
method, that’ll dispatch the autoLogin
from our store every time the app is loaded.
created () { this.$ store.dispatch('AutoLogin') }
Fetch_User_Data
We want to welcome the user on the dashboard by displaying the user’s name. And so, another action called fetchUser
is created which first checks if there’s a token as usual. Then, it goes on to get the email from local storage and queries the database as done earlier with the email validation.
This returns an object containing the user’s data initially submitted during sign-up. We then convert this object into an array and commit it to the storeUser
mutation initially created.
fetchUser ({ commit, state}) { if (!state.idToken) { return } const email = localStorage.getItem('email') axios.get('https://vue-journal.firebaseio.com/users.json?orderBy="email"&equalTo="' + email + '"') .then(res => { console.log(res) // const users = [] console.log(res.data) const data = res.data const users = [] for (let key in data) { const user = data[key] user.id = key users.push(user) console.log(users) } commit('storeUser', users[0]) }) .catch(error => console.log(error)) }
After which we create another getter called user
which returns the state.user
already committed through storeUser
.
getters: { user (state) { return state.user }, isAuthenticated (state) { return state.idToken !== null } }
Back to the dashboard, we create a new computed method called name
that returns state.user.name
only if the user exists.
computed: { name () { return !this.$ store.getters.user ? false : this.$ store.getters.user.name } }, created () { this.$ store.dispatch('fetchUser') } }
And we’ll also add the created
computed property to dispatch the fetchUser
action once the page is loaded. We then use the v-if
in our HTML in order to display the name if the name exists.
<p v-if="name">Welcome, {{ name }} </p>
Send_Survey
To send the survey, we’ll create a postData
action that sends the data to the database using the database API, with the token to show the server that the user is logged in.
postData ({state}, surveyData) { if (!state.idToken) { return } axios.post('https://vue-journal.firebaseio.com/survey.json' + '?auth=' + state.idToken , surveyData) .then(res => { console.log(res) }) .catch(error => console.log(error)) }
We come back to the dashboard component and dispatch the data to our postData
action in the store.
methods : { onSubmit () { const postData = { price: this.price, long: this.long, comment: this.comment } console.log(postData) this.$ store.dispatch('postData', postData) } }
There we have it, we’ve got a lot of useful features implemented into our demo application while communicating with our Firebase server. Hopefully, you’ll be using these powerful features in your next project as they’re very critical to building modern web apps today.
If you have any questions, you can leave them in the comments section and I’ll be happy to answer every single one of them!
- The demo for the tutorial is live here.
Other resources that may prove useful includes:
- To understand more about Firebase and the other services it offers, check out Chris Esplin’s article, “What Is Firebase?”
- Vuelidate is a really nice library you should really dig into. You should read through its documentation to gain full insight.https://vuelidate.js.org/.
- You can also explore axios on its own, especially if you want to use it in bigger projects.
Articles on Smashing Magazine — For Web Designers And Developers