DEV Community

Cover image for To the Stars with Quasar & Firebase - User Profile
Adam Purdy for Quasar

Posted on • Updated on

To the Stars with Quasar & Firebase - User Profile

Table Of Contents

  1. Introduction
  2. Update User Creation Flow
  3. User Profile with Vuexfire
  4. Update User Info
  5. Unbinding Vuexfire Bindings
  6. Summary
  7. Repository

1. Introduction

This article builds atop of the initial article, Initial Service & Structure, and Email Authentication published in the Quasar-Firebase series. In this post, we're going to build something that most applications that handle users have: a user profile. We'll be using Firebase, Firestore, Vuexfire, and Google's Cloud Storage.

  • 1.1 Assumptions

Before we get started, a few assumptions are in order. In this post, we're going to be building atop of the email authentication article, specifically, state management. Be sure to go over the insights in that post first, or review if needed. Also, one minor addition is we'll be adding Vuexfire bindings in our actions files. Let's update our illustration for consistency.

Alt Text

*Note: This repo already contains a working Firebase API key. In order to set up your own project, you need to delete the "FIREBASE_CONFIG" attributes within the .quasar.env.json file and replace it with your own key from the first article.

If you already created an account on this Firebase API from the email authentication article you'll need to use a different email to set up a new account, as that account doesn't have a user in the users collection as you'll learn in this article.*

Be sure to clone the repo and have the app to follow along with. Navigate to the respective app and run:

$ npm run dev
Enter fullscreen mode Exit fullscreen mode

A final note, this code is for Vue v2 and Quasar v1.

2. Update User Creation Flow

In our email authentication post, we built a form to allow users to register a new account and also log into the application via their credentials that were supplied during registration. That was a good first step, but now we need to expand the experience so we can build our profile off of additional user information.

Also, now that we're going to be working more in-depth with our user, we're going to split up our layouts. One called Basic.vue, and one called User.vue. The user layout will have the logging out functionality and also controlling the opacity of when a user launches their settings modal.

/src/layouts/Basic.vue
/src/layouts/User.vue

Managing users are possible to some extent through the Firebase authentication record, but we need more fidelity for our user.

Let's use Cloud Firestore as our database and create a new record in a users' collection.

  • 2.1 Setup Cloud Firestore

Back in the Firebase console, click on the Database section in the left menu, and then create a Cloud Firestore instance.
Alt Text

Be sure to keep our Firestore instance in Production Mode
Alt Text

Set the location of your server. You can choose whatever works best for your location.
Alt Text

Once you have Firestore set up, it's essential to set some basic security rules for your data. In your Rules tab, enter the following snippet of code:

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

Alt Text

If you don't do this, your call to Firestore to save a User record will fail, and our application will prevent the user from moving forward after successful user registration.
Alt Text

  • 2.2 Create the DB Service

Now, a DB service needs to be created. Take a look at the new service:

/src/services/firebase/db.js

Following the pattern from the Email post, this service allows the application to have a reference to Cloud Firestore. Now, add the reference of the db service into our firebaseService object in our index.js file to keep the service under one namespace, firebaseService.

/src/services/firebase/index.js

import * as base from '../services/firebase/base.js'
import * as db from '../services/firebase/db.js'
import * as email from '../services/firebase/email.js'

export default Object.assign({}, base, db, email)
Enter fullscreen mode Exit fullscreen mode
  • 2.3 User Model

Next, create a User model.

/src/models/User.js

/** Class representing a User. */
export default class User {
  /**
   * Create a user.
   * @param {String} id - Place holder for a unique Firebase id.
   * @param {String} backgroundPhoto - A generated URL from Google Storage.
   * @param {String} email - A valid email.
   * @param {String} fullName - The user's full name.
   * @param {String} mobile - the user's mobile number.
   * @param {String} profilePhoto - A generated URL from Google Storage.
  */
  id = ''
  backgroundPhoto = ''
  email = ''
  fullName = ''
  mobile = ''
  profilePhoto = ''

  /**
   * @param  {Object} args - User args
   */
  constructor (args) {
    Object.keys(args).forEach((v, i) => {
      this[v] = args[v]
    })

    return {
      ...this
    }
  }
}

Enter fullscreen mode Exit fullscreen mode

Here is a basic user class that consumes the supplied arguments and returns an object to send to Cloud Firestore.

  • 2.4 Update Auth Actions

Now that there is a user object, the actions file for creating a user can be updated. Assuming email is the authentication method, let's look at the actions file.

/src/store/auth/actions.js

Similar to the actions file from the last post for doing basic email authentication, a few changes are needed. Import the new User class, add a new method, addUserToUsersCollection, and then update our existing method, createNewUser.

import { firestoreAction } from 'vuexfire'
import User from '../../models/User.js'

export const addUserToUsersCollection = async (state, userRef) => {
  const
    { email } = state,
    user = new User({ email })
  return userRef.set(user)
}

export const createNewUser = async function ({ dispatch, commit }, data) {
  const $fb = this.$fb
  const { email, password } = data
  const fbAuthResponse = await $fb.createUserWithEmail(email, password)
  const id = fbAuthResponse.user.uid
  const userRef = $fb.userRef('users', id)
  return addUserToUsersCollection({ email }, userRef)
}
Enter fullscreen mode Exit fullscreen mode

A quick note before we move on.

  • The import of firestoreAction is seen later in our updated logoutUser action.

  • Accessing $fb from this is possible because of the use of the function keyword, and because we assigned the service back in our serverConnection file when we imported * for base, email, and db, respectively.

3. User Profile with Vuexfire

Now that we have the user flow updated, we move the user over to the user profile screen upon the successful creation of a new user record in Firestore via our route command via our Auth.vue file.

/src/pages/Auth.vue

onSubmit () {
  const { email, password } = this
  this.$refs.emailAuthenticationForm.validate()
    .then(async success => {
      if (success) {
        this.$q.loading.show({
          message: this.isRegistration
            ? 'Registering your account...'
            : 'Authenticating your account...',
          backgroundColor: 'grey',
          spinner: QSpinnerGears,
          customClass: 'loader'
        })
        try {
          if (this.isRegistration) {
            await this.createNewUser({ email, password })
          } else {
            await this.loginUser({ email, password })
          }
          this.$router.push({ path: '/user/profile' })
        } catch (err) {
          console.error(err)
          this.$q.notify({
            message: `An error as occured: ${err}`,
            color: 'negative'
          })
        } finally {
          this.$q.loading.hide()
        }
      }
    })
}

Enter fullscreen mode Exit fullscreen mode

Here is our profile page.

/src/pages/user/Profile.vue

Before we render the user profile, we want to get the user's data and sync it to our application's store via Vuexfire.

  • 3.1 Why Vuexfire

The Vue core team maintains Vuexfire, so a reasonable assumption here is that their approach to syncing your data against Firestore is well-designed. Vuefire, another similar binding available, is another option. However, over time, as your application grows, and the need for its data to be in your application's store for multiple aspects of the app, it's just easier to keep it in Vuex.

Ideally, we want to go from this:
Alt Text

to this, with as little as code possible.
Alt Text

The Firebase SDK does provide an API to keep your local data in sync with any changes happening in the remote database. However, it is more tedious than you can imagine, and it involves many edge cases. Take a look here at the code needed to perform this operation.

  • 3.2 Vuexfire Installation

Simply install Vuexfire in your app:

yarn add vuexfire
# or
npm install vuexfire
Enter fullscreen mode Exit fullscreen mode
  • 3.3 Vuexfire Integration

Next, integrate the binding package into our store.

/src/store/index.js

import Vue from 'vue'
import Vuex from 'vuex'
import { vuexfireMutations } from 'vuexfire'

import auth from './auth'
import common from './common'
import user from './user'

Vue.use(Vuex)

/*
 * If not building with SSR mode, you can
 * directly export the Store instantiation
 */

export default function (/* { ssrContext } */) {
  const Store = new Vuex.Store({
    modules: {
      auth,
      common,
      user
    },
    mutations: {
      ...vuexfireMutations
    },

    // enable strict mode (adds overhead!)
    // for dev mode only
    strict: process.env.DEV
  })

  return Store
}

Enter fullscreen mode Exit fullscreen mode

Now that we have the binding connected to our store, we will create a method in the store's user module actions file.

/src/store/user/actions.js

export const getCurrentUser = firestoreAction(({ bindFirestoreRef }, id) => {
  return bindFirestoreRef('currentUser', userRef('users', id))
})
Enter fullscreen mode Exit fullscreen mode

Now that we have an access point to get our current user information from our users' collection in Firestore, we'll need to connect this method when Firebase's onAuthStateChanged observer fires when the user becomes authenticated. The key to this is setting our auth state with a uid from Firebase. Then the key can be used anywhere in the app where specific data regarding the user is needed.

Back in our base.js service, look at the handleOnAuthStateChanged method:

/src/services/firebase/base.js

export const handleOnAuthStateChanged = async (store, currentUser) => {
  const initialAuthState = isAuthenticated(store)
  // Save to the store
  store.commit('auth/setAuthState', {
    isAuthenticated: currentUser !== null,
    isReady: true,
    uid: (currentUser ? currentUser.uid : '')
  })
Enter fullscreen mode Exit fullscreen mode

Remember this method is connected to our serverConnection boot file.
/src/boot/serverConnection.js

firebaseService.auth().onAuthStateChanged((currentUser) => {
    firebaseService.handleOnAuthStateChanged(store, currentUser)
  }, (error) => {
    console.error(error)
  })
Enter fullscreen mode Exit fullscreen mode

Once the uid is available via our currentUser from our Firebase auth service, we can attach it to our auth state, and commit the mutation in our
handleOnAuthStateChanged method.

/src/store/auth/mutations.js

export function setAuthState (state, data) {
  state.isAuthenticated = data.isAuthenticated
  state.isReady = data.isReady
  state.uid = data.uid
}
Enter fullscreen mode Exit fullscreen mode

From here, a decision needs to be made when to query Firestore for the user's data. Either here in the handleOnAuthStateChanged, or later once the protected route has passed the route guard checks, and then perform the query and notify the user that the app is fetching data. In this instance, we're going to start the query here in the base service for the user's profile data. Because we've added the uid to the auth state, we can still rely on the uid to be available to any protected route before the page renders. This gives any protected view the key needed to query any data related to the user before rending the view, and after Firebase has supplied the uid from its Auth service.

export const handleOnAuthStateChanged = async (store, currentUser) => {
// ...

// Get & bind the current user
  if (store.state.auth.isAuthenticated) {
    await store.dispatch('user/getCurrentUser', currentUser.uid)
  }

// ...
}

Enter fullscreen mode Exit fullscreen mode

Once the dispatch has completed, the application's currentUser is now bound to our Vuex store.

And that's it! Now, all subsequent writes to our user document in Firestore will automatically be kept in sync in our store module with no additional coding.

5. Updating User Info

At this point, you should have an app that looks like the image below.
Alt Text

Also, if you open up dev tools you will see a console statement outputting the uid from our state that is available to the protected page before rendering from our created Vue lifecycle method in our User.vue file.

Alt Text

Now that we have our data from Firestore bound and in-sync, we're ready to move on to the final piece of our user profile feature, uploading files, and updating user fields.

  • 5.1 Google Cloud Storage Setup

Head back over to the console and click on the storage menu item, and click Get started, and follow the rest of the prompts.
Alt Text

Alt Text

Alt Text

  • 5.2 User actions

Now that the current user called from Firestore is loaded into our store's user module, it's time to upload a photo to Cloud Storage. First, take a look at the custom component based off of Quasar's q-uploader, within the UserSettings component.

/src/pages/user/profile/UserSettings.vue

Per the docs, we can create a custom component to support our Cloud Firestore service modeled after the QUploaderBase mixin.

Have a look at our custom component FBQUploader

Because there are some considerations regarding reusability, multiple file uploads, and other considerations, a separate article highlighting FBQUploader component will be available in the future.

In regards to the user profile content like name, phone number, or anything else for that matter, capture the data and post it to Firestore. Here we can see this in the UserSettings component again. First, we capture the data in our saveUserData method on the form submission.

async saveUserData () {
  const { currentUser, email, fullName, mobile } = this

  this.$q.loading.show({
    message: 'Updating your data, please stand by...',
    customClass: 'text-h3, text-bold'
  })

  try {
    await this.updateUserData({
      id: currentUser.id,
      email,
      fullName,
      mobile
    })
  } catch (err) {
    this.$q.notify({
      message: `Looks like a probelm updating your profile: ${err}`,
      color: 'negative'
    })
  } finally {
    this.$q.loading.hide()
    this.setEditUserDialog(false)
  }
}
Enter fullscreen mode Exit fullscreen mode

Set up some visual language notifying the user that we're doing an update via Quasar's Loading plugin, massage the data, then pass it over to the user action, updateUserData.

export const updateUserData = async function ({ state }, payload) {
  return userRef('users', payload.id).update(payload)
}
Enter fullscreen mode Exit fullscreen mode

Again, once the data is successfully stored in the current user document in the users' collection, your store's user module automatically updates via the Vuexfire binding.

6. Unbinding Vuexfire Bindings

Lastly, when the user logs off we need to unbind our Vuexfire bindings.

export const logoutUser = async function ({ commit }, payload) {
  await firestoreAction(({ unbindFirestoreRef }) => { unbindFirestoreRef('users') })
  commit('user/setCurrentUserData', null, { root: true })
  await this.$fb.logoutUser()
}
Enter fullscreen mode Exit fullscreen mode

7. Summary

Hopefully, this gave you some insight into the process of creating a user record in Firestore from the Firebase authentication step. Also, setting up a common use case of a user profile while combining the functionality of uploading files to Google's Cloud Storage, as well as harnessing the power of Vuexfire bindings to simplify the syncing process between your Vuex store modules and Cloud Firestore.

8. Repository

User Profile Repo

Top comments (13)

Collapse
 
eosca profile image
eosca

Hello! I have a question. Why are you sometimes using vuex actions (insted of simple functions outside actions file, outside the store) if those actions are not commiting mutations? Are they actions or not?

Ex.: in file store/auth/actions.js -> routeUserToAuth, loginUser, or createNewUser...

Thanks for your work with firebase-quasar!

Collapse
 
adamkpurdy profile image
Adam Purdy • Edited

Hello, thanks for the question, and sorry for the late response. I’ve been away from my computer the past couple of months doing a bathroom remodel.

It’s more of a convention thing. You can easily do how you mentioned in a utility file or something else you like.

I tend to keep any interactions with the DB in actions, and technically most of those functions you mentioned touch Firebase and alter the user’s state. Keep in mind the use of the vuexfire. Once the user has logged in, a call is being made to the user’s collection or adding a new user, in the case of the createNewUser, which will get the currentUser’s info and put it into state via vuexfire.

The exception here is routeUserToAuth. Again, putting that function in an action file is just convention, as it’s a small sample app. I hope that answer's your question.

Collapse
 
eosca profile image
eosca

Ok. Thank you! And good luck with reform.

Collapse
 
djsilvestri profile image
David

Thanks for the tutorial. How would I get the currentUser uid into my node server? I think this goes into my index.js file:
admin
.auth()
.verifyIdToken(idToken)
.then((decodedToken) => {
const uid = decodedToken.uid;

})
.catch((error) => {
// Handle error
});

But where do I put the code to get the token from the client?

firebase.auth().currentUser.getIdToken(/* forceRefresh */ true).then(function(idToken) {
// Send token to your backend via HTTPS
// ...
}).catch(function(error) {
// Handle error
});

Collapse
 
adamkpurdy profile image
Adam Purdy

Set up your axios boot file with an intercepter.

import axios from "axios"
import firebaseService from "../services/firebase"

const api = axios.create({
  baseURL: "http://localhost:5000/api/v1"
})

export default ({ store, Vue }) => {
  api.interceptors.request.use(async req => {
    const token = await firebaseService.auth().currentUser.getIdToken()
    req.headers.Authorization = `Bearer ${token}`
    return req
  })

  Vue.prototype.$api = api
  store.$api = axios
}
Enter fullscreen mode Exit fullscreen mode
Collapse
 
djsilvestri profile image
David

Hi Adam,
I appreciate the quick reply. I included that interceptor in my axios boot file. So the token can then be passed into index.js and then I can verify the user, but I am not understanding how to get the current user's uid into my node file...

Sorry for my ignorance, still learning!

Thread Thread
 
adamkpurdy profile image
Adam Purdy

No problem!

Pass it from the client app via the API call by retrieving it from the currentUser that is loaded in your page from your Vuex getter.
Profile

You can extract out the uid from that currentUser object and then when you make your API call to your node server include it in your payload.

If this still doesn't make sense it's best if you reach out to me on Discord (Adam P(EN/US)) as you might need clarification on working with the data flow.

Thread Thread
 
djsilvestri profile image
David

Sounds good! Would you mind adding me as a friend? djsilvestri#5594

Collapse
 
eosca profile image
eosca

Hello, your tutorial helps me a lot, thanks again!!!

I have another question... why did you code "return { ...this }" in the constructor of the class User??? I was copying your code in each class I was creating for my app and when I needed to invoke public methods I couldn't because of that. Now it's solved deleting the return of the constructor, but I am asking myself about the reason of that. Is there any reason?

Collapse
 
eosca profile image
eosca

Firestore needs js pure objects... ok further problems help me to know why "return { ...this }"...

Collapse
 
adamkpurdy profile image
Adam Purdy

Hello again. I typically do not use Classes in my javascript as I do not come from a classical programming background. I used a class here to abstract the user model, but I still use just a standard pure js object for object creations for Firestore as well.

In regards to your question: why return { ...this }.

When the user object is instantiated in the auth actions file, the email is being provided to the constructor. After the constructor has finished its loop and populated the prop or email, it then returns a pure js object by using the spread operator to populate all of the initial props and the update email prop.

I can not speak into why your public methods are not working, but feel free to move away from this pattern of setting up the User via a class or modify it to suit your needs.

Collapse
 
gicelte profile image
gicelte

Hi, thanks for the great article! I am interested in implementing role-based authentication with Firebase. Could you give some hints about how to extend the code you have provided to get a system able to manage administrators and normal users? Is this related to David's question below?

Collapse
 
adamkpurdy profile image
Adam Purdy

Hey @gicelte you need to implement the setCustomUserClaims on the server side of things via Firebase Admin.

On my service side of things in my applications I just add a simple prop in the method call.

await admin.auth().setCustomUserClaims(authUser.uid, {
  isAdmin: true
})
Enter fullscreen mode Exit fullscreen mode

Then will do an await call on the client to route the user based on perms set on the user's auth record coming back from Firebase auth.

This is a start but by no means a thorough approach as user roles can be extensive in an application. For an in-depth setup explanation of setCustomUserClaims try googling: 'role based auth with firebase admin tutorial'.

Hope that gave you enough to point you in the right direction.