Build A CRUD To-Do App With Authentication Using Vue JS & Firebase

Last modified on August 7th, 2023
Raja Tamil
Firebase Vue.js

In this Firestore CRUD Vue.js tutorial, I will be guiding you on how to build a real-world To-Do web app with Firebase Authentication.

This is the second part of the Firestore CRUD Vue.js tutorial.

Part #1: Firebase Authentication for Vue.js

Part #2: Build A Secure To-Do App with Vue.js + Firestore (you’re here)

What are we building?

Let’s get started 🚀

If you already have the vue.js project running and added Firebase to your project from the Firebase Authentication for Vue.js tutorial, skip to Create a Component and Route for To-Do View.

Up and Running Vue.js Starter Project

Go ahead and download the starter vue.js project.

CD to the project on your Terminal, and run the following command:

npm install

Once the dependencies installation is complete, go ahead and launch the app by going to the provided localhost URL.

If everything is good, you should have an app running like this:

firestore-crud-app-start-vue

Configure Firebase to the Vue.js Project

Once the vue project is up and running, the next step will be to add Firebase to your project by going to main.js and replace config code from your Firebase project.

const firebaseConfig = {
  apiKey: "****************************",
  authDomain: "****************************",
  databaseURL: "****************************",
  projectId: "****************************",
  storageBucket: "****************************",
  messagingSenderId: "****************************",
  appId: "****************************"
};

If OAuth login buttons such as Facebook and Google do not work, you will have to configure them which are covered in the Firebase Authentication for Vue.js tutorial.

Create A Todo Component and Route

In your vue.js project, go to the srccomponents → create Todos.vue file.

Then, add the scaffold code.

<template>
</template>

<script>
export default {
}
</script>

<style>
</style>


Once it’s done, switch to router folder → index.js file and import the component at the top.

import Todos from '@/components/Todos'

Then, add a new object for todo inside the router array.

{
    path: '/todo',
    name: 'Todos',
    component: Todos,
    meta: {
        auth: true
    }
}

Set auth:true flag in this todo route object, which will make sure only authenticated users have access to the todo page component.

Switch back to ToDo.vue file and add HTML code for navigation and heading inside between the starting and ending template tags.

<section>
    <navigation></navigation>
    <h5 class="center-align">To-Dos</h5>
</section>

I am using Materialize CSS Framework in this project in case you are wondering.

Moving on to the script section.

At the top, a <navigation> component is added.

<script> 
import navigation from "@/components/NavBar.vue"; 
export default { 
  components: { navigation }
}; 
</script>

Then, import NavBar.vue and add it inside the components object.

It would be nice to have a navigation item at the top of the todos page. Hop over to NavBar.vue and add the following code inside the ul element.

<li v-show="user">
  <router-link to="/todo">To Do</router-link>
</li>

At this stage, you can access the todo page by going to http://localhost:8080/todo.

The port of your localhost can be different. In my case, it’s 8080.

Before diving into the application, let’s take a moment and think about how to structure / model data for the To-Do application.

Structure Firestore Data for the Todo App 

It’s really important to have an idea of how to model or structure your Firestore data before starting to write the code.

This is what I came up with:

Firestore Data Modeling To Do App

Firestore Data Modeling To-Do App

First, create a top-level collection called users and add currently logged in user’s uid as the document ID. You can easily obtain uid using firebase.auth().uid.

The main reason for using uid instead of an auto-generated ID is to protect each user’s data using Firebase Rules, which I will cover in the Secure To-Do App Using Firestore Rules section.

Inside each user’s document, create a sub-collection called todos which will have multiple todos documents with some fields like title, isCompleted etc.

Pretty straight forward! 🙂

There are multiple ways to structure data for this project. Feel free to let me know if you find a better solution for this app.

Create User-Specific Data To Cloud Firestore

The first step is to add the HTML code snippet under the heading in the ToDo.vue file.

It has a ul with a single list-item which is the list head.

<ul class="collection with-header">
    <li class="collection-header">
        <div class="row">
            <div class="input-field col s10">
                <input id="new_todo" type="text" class="validate" v-model="todo.title" />
            </div>
            <div class="input-field col s2">
                <button class="btn" @click="addTodo">Add</button>
            </div>
        </div>
    </li>
</ul>

As you can see, I have an input field that binds the todo.title and an add button is bounded to a click event with a call back function called addTodo().

Adding the following CSS code will make the ul have a width of 500px and center to the canvas.

<style>
.collection.with-header {
    max-width: 500px;
    margin: 0 auto;
}
</style>


Next, declare the todo object with a title property.

data() {
    return {
        todo: {
            title: "",
        }
    };
}

Then, import firebase at the top inside the JavaScript section.

import firebase from "firebase";

Finally, define the addTodo() callback function inside methods:{} and make a query to add a new todo data to the Cloud Firestore.

addTodo() {
    firebase
        .firestore()
        .collection("users")
        .doc(firebase.auth().currentUser.uid)
        .collection("todos")
        .add({
            title: this.todo.title,
            createdAt: new Date(),
            isCompleted: false,
        })
}

Check out Learn Firestore CRUD Queries in case you want to brush up on it.

Let’s take a look at the Firebase query for adding new data.

Get the reference to the users collection and invoke doc() method on it by passing currently logged in user’s uid as an argument.

After that, obtain a reference to a sub-collection called todos and add a document using add() method by passing a JavaScript object that has three key-value pairs:

  • title:string
  • createdAt:timestamp
  • isCompleted:boolean

At this stage, the app will look like this below.

todo-add-form

Let’s add a new todo item.

firestore-added-data

Nice!

Next, retrieve data from the Cloud Firestore.

Get User-Specific Data with Cloud Firestore

The first step is to define todos array that will contain todo objects of a currently logged in user.

data() {
    return {
        todos: [],
        todo: {
            title: ""
        }
    };
}

Next, declare getTodos() function inside methods:{}

In the code below, I am using async await syntax for this asynchronous operation.

This query is very similar to the add() one. Get a reference to the users collection and invoke doc() method by passing currently logged in user’s uid as an argument.

async getTodos() {
    var todosRef = await firebase
        .firestore()
        .collection("users")
        .doc(firebase.auth().currentUser.uid)
        .collection("todos");

    todosRef.onSnapshot(snap => {
        this.todos = [];
        snap.forEach(doc => {
            var todo = doc.data();
            todo.id = doc.id;
            this.todos.push(todo);
        });
    });
}

Then, get a reference to the todos sub-collection and store the query object to a variable called todosRef.

Run onSnapshot() method on the todoRef query object. I could use get() but I want to see the real-time change to the view as Cloud Firestore Database changes.

Inside the onSnapshot() method, iterate through the snap object and set the document data using doc.data() to a variable called todo. Also, add an ID property to that object and set it to auto-generated ID using doc.id.

Finally, push the todo object to the todos array that was created earlier.

Make sure to reset the todos array, which is on the first line inside the onSnapshot() method to avoid duplicated data.

Now, invoke getTodos() function inside created() {} method.

created() {
    this.getTodos();
},

The third step is to add HTML code.

Inside the unordered list, after the first header style list-item, add the following:

<li class="collection-item" v-for="todo in todos" :key="todo.id">
  {{todo.title}}
</li>

Iterate through the todos array using v-for and show the title using todo.title.

Pretty straight forward!

Update User-Specific Data in Cloud Firestore

It would be nice to have a checkbox on the right side of each todo list item to allow users to check when it is done.

To make it happen, add checkbox input to an existing HTML code.

<li class="collection-item" v-for="todo in todos" :key="todo.id" :class="{ fade: todo.isCompleted }">
    {{todo.title}}
    <span class="secondary-content">
        <label>
            <input type="checkbox" class="filled-in" :checked="todo.isCompleted"
                @change="updateTodoItem(todo.id, $event)" />
            <span></span>
        </label>
    </span>
</li>

There will be a change event attached to each todo list item. The callback function for the change event is updatedTodoItem() that takes two arguments.

The first one is the auto-generated ID of the todo list item that I can access using todo.id and the second one is an event object.

Let’s declare a change event callback function updateTodoItem() inside methods:{} object.

updateTodoItem(docId, e) {
    var isChecked = e.target.checked;
    firebase
        .firestore()
        .collection("users")
        .doc(firebase.auth().currentUser.uid)
        .collection("todos")
        .doc(docId)
        .update({
            isCompleted: isChecked
        });
},

When a user clicks the checkbox, capture the checked status using e.target.checked and set it to a variable isChecked.

As you can see, this query is very similar to the previous one.

Use the update() method to change the value of isCompleted by assigning isChecked value to it.

If you click the checkbox at this stage, you can see the isCompleted value changes to true on the Cloud Firestore.

The change will reflect immediately on the view.

The persistence is happening because I have set todo.isCompleted value to :checked HTML property.

update-todo-firestore

Nice.

Let’s move on to how users can delete their own todo list items.

Delete User Data from Cloud Firestore

Add a span element inside the list item with a class named deleteIcon.

<li class="collection-item" v-for="todo in todos" :key="todo.id" :class="{ fade: todo.isCompleted }">
    <span class="deleteIcon" @click="deleteToDo(todo.id)">✕</span>
    {{todo.title}}
    <span class="secondary-content">
        <label>
            <input type="checkbox" class="filled-in" :checked="todo.isCompleted"
                @change="completedPressed(todo.id, $event)" />
            <span></span>
        </label>
    </span>
</li>

Attach a click event to the span element with a callback function deleteTodo() and pass an ID to it as an argument.

Then, create deleteTodo() function inside methods:{}.

deleteToDo(docId) {
    firebase
        .firestore()
        .collection("users")
        .doc(firebase.auth().currentUser.uid)
        .collection("todos")
        .doc(docId)
        .delete();
}

Delete is pretty straight forward and all you have to do is to invoke delete() method to an appropriate document and done!

Here is the CSS for the delete section.

.deleteIcon {
    margin-right: 10px;
    cursor: pointer;
}
.deleteIcon:hover {
    opacity: 0.5;
}


At this stage, the todo CRUD app is fully functional.

But…

It’s NOT secure!

not-secure

Secure Users Data Using Firestore Rules

The REAL security lies on the Firebase Rules.

Let me say it again.

The REAL security lies on the Firebase Rules.

Look at the current security rules by going to the Firebase ConsoleAuthenticationRules Tab

This security rule will allow anyone to have the ability to read or write to any location to the Cloud Firestore Database which is good for development and testing but definitely NOT for production.

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
       allow read, write;
    }
  }
}

Let’s change the Firebase Rules so that only logged in users can read or write to the Database like below. You can access uid using request.auth.uid and check to see if it exists.

service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
         allow read, write: if request.auth.uid != null;
    }
  }
}

This will make sure that only authenticated users can have permission to read or write (create, update and delete) to the Cloud Firestore Database.

But, it can let any user modify any other users data.

For example, let’s say userA is logged in and added some todo items. UserB is logged with a different machine and also added some todos. If userA hacks UserB’s uid from the browser, UserA can get UserB’s data as long as UserA is logged in, which is INSECURE.

To prevent this, the Firebase Rule should only authorize users to modify their own data, not others.

service cloud.firestore {
    match /databases/{database}/documents {
        match /users/{uid}/todos/{todoId} {
            allow read, write: if request.auth.uid == uid
        }
    }
}

Now, only authorized users can modify their data in the following database path users/{userId}/todos/{todoId} as long as the userId on the users collection matches to the currently logged in user’s uid.

There you have it! 🙂

Conclusion

In this Firestore CRUD Vue.js tutorial, you have learned how to do CRUD queries with Firestore by building a fully functional To-Do Application.

Then, I showed you how to secure the To-Do app using Firestore Rules.

Download the sample source code on Github.

NEXT → Build A Custom Payment Form using Stripe + Cloud Functions