Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Initialize Vue app after Firebase auth state changed

I have a bit of a conundrum in my Vue/vuex/vue-router + Firebase app. The bootstrap code is supposed to initialize Firebase, as well as the signed in user (if any) in Vuex:

new Vue({
  el: '#app', router, store, template: '<App/>', components: { App },
  beforeCreate() {
    firebase.initializeApp(firebaseConfig)
      // Because the code below is async, the app gets created before user is returned...
      .auth().onAuthStateChanged(user => {
        this.$store.commit('syncAuthUser', user) // this executes too late...
      })
  }
})

In my routes, I have

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

as well as

router.beforeEach((to, from, next) => {
  let authUser = store.getters.authUser // this is not set just yet...

  if (to.matched.some(record => record.meta.requiresAuth)) {
    if (!authUser) {
      next('signin') // /home will always hit here...
    }
    next()
  }
  else {
    next()
  }
})

Problem

As mentioned in the comments, the app gets the authenticated user too late. So, when you go to /home while logged in, the app will run the beforeEachcheck in the routes... And the user instance will not be available just yet, so it will think that you are not logged in, when in fact you are (it's clear from my computed properties that are properly updated a few moments later).

Question

So, how do I change my code, so that the Vue app gets initialized only after Firebase returns the user and after that user is passed to the Vuex store? Plese let me know if I am doing something completely wrong.

Attempts

  1. I tried the "unsubscribe" approach; however, its problem is that the onAuthStateChanged would only be triggered once. So, if I put my crucial this.$store.commit('syncAuthUser', user) there, then that code would only execute when the app is created, and not when the user logs out for example.
  2. I also tried a better approach here. I did work for me, but I felt bad wrapping onAuthStateChanged in another Promise, for sure there must be a better way; plus, it seems wrong to place the auth user initialization login in the routes file.
like image 776
Alex Avatar asked Jul 20 '17 02:07

Alex


People also ask

How do I add authentication to a Vue app using firebase?

Setting up Firebase Auth Here, click on Add new project and name the project vue-firebase-auth . You will be redirected to the project's dashboard. On the dashboard, click on the web icon to register your front-end app. Name your app vuex-firebase-authentication and click on Register app .

What is Vuefire?

Vuefire is a small and pragmatic solution to create realtime bindings between a Firebase RTDB or a Firebase Cloud Firestore and your Vue application. Making it straightforward to always keep your local data in sync with remotes databases.

What is AuthState?

public class AuthState extends Object. Collects authorization state from authorization requests and responses. This facilitates the creation of subsequent requests based on this state, and allows for this state to be persisted easily.


1 Answers

The easiest way is to wait for the auth state before mounting your app:

const app = new Vue({
  router,
  store,
  render: h => h(App),
});

firebase.auth().onAuthStateChanged(user => {
  store.commit('authStateChanged', user);
  app.$mount('#app');
});

If you want to show a spinner before mount put it in #app:

<div id="app"><div class="loader">Loading...</div></div>

In fact it will make your actions cleaner and the app more reliable if you extract the auth state logic to a authStateChanged commit as above, and handle your route redirection here too. As this will handle if the auth state changes while you are on a route while router.beforeEach will check when you're going to one.

firebase.auth().onAuthStateChanged(user => {
  store.commit('auth/authStateChanged', user);

  const isAuthed = Boolean(user);
  const { meta } = router.history.current;

  if (isAuthed && meta.isPublicOnly) { // e.g. /login + /signup
    router.push('/dashboard');
  } else if (!isAuthed && meta.isPrivate) {
    router.push('/login');
  }

  app.$mount('#app');
});

Note that the first router.beforeEach callback happens before this, so you should ignore it until auth state has been determined. Since we handle the first redirection in onAuthStateChanged it doesn't matter.

state: {
  isInited: false,
  isAuthed: false,
},
mutations: {
  authStateChanged(state, user) {
    state.isAuthed = Boolean(user);
    state.isInited = true;
  }
}

router.beforeEach((to, from, next) => {
  const { isInited, isAuthed } = store.state.user;

  if (isInited) {
    if (!isAuthed && to.matched.some(record => record.meta.isPrivate)) {
      return next('/login');
    } else if (isAuthed && to.matched.some(record => record.meta.isPublicOnly)) {
      return next('/dashboard');
    }
  }

  next();
});

You could probably prevent the route loading at all until isInited is true alternatively.

like image 69
Dominic Avatar answered Nov 15 '22 21:11

Dominic