Warm tip: This article is reproduced from stackoverflow.com, please click
firebase google-cloud-firestore google-cloud-functions firebase-authentication

How to complete login only after functions.auth.user().onCreate is finished

发布于 2020-03-27 15:44:00

I'm using firebase functions and I have a function which add new collection when user is creating. The problem is sometimes user is logged in before function is done, so user is logged in but new collection is not created yet (and then I have error message 'Missing or insufficient permissions. because a rule cannot find that collection'). How can I handle it?

Is it possible to finish login user (for example using google provider) only when all stuff from

export const createCollection = functions.auth.user().onCreate(async user => {
  try {
    const addLanguages = await addFirst();
    const addSecondCollection = await addSecond();

    async function addFirst() {
      const userRef = admin.firestore().doc(`languages/${user.uid}`);
      await userRef.set(
        {
          language: null
        },
        { merge: true }
      );

      return 'done';
    }

    async function addSecond() {
      // ...
    }

    return await Promise.all([addLanguages, addSecondCollection]);
  } catch (error) {
    throw new functions.https.HttpsError('unknown', error);
  }
});

is finished? So google provider window is closed and user is logged in only after that? (and don't using setTimeouts etc)

Questioner
Przemo
Viewed
76
Renaud Tarnec 2020-01-31 21:13

AFAIK it is not possible to directly couple the two processes implied in your application:

  • On one hand you have the Google sign-in flow implemented in your front-end (even if there is a call to the Auth service in the back-end), and;
  • On the other hand you have the Cloud Function that is executed in the back-end.

The problem you encounter comes from the fact that as soon as the Google sign-in flow is successful, your user is signed in to your app and tries to read the document to be created by the Cloud Function.

In some cases (due for example to the Cloud Function cold start) this document is not yet created when the user is signed in, resulting in an error.

One possible solution would be to set a Firestore listener in your front-end to wait for this document to be created, as follows. Note that the following code only takes into account the Firestore document created by the addFirst() function, since you don't give any details on the second document to be created through addSecond().

firebase.auth().signInWithPopup(provider)
.then(function(result) {
  var token = result.credential.accessToken;
  var user = result.user;  
  //Here we know the userId then we can set a listener to the doc languages/${user.uid}

  firebase.firestore().collection("languages").doc(user.uid)
    .onSnapshot(function(doc) {
       if(doc.exists) {
         console.log("Current data: ", doc.data());
         //Do whatever you want with the user doc
       } else  {
         console.log("Language document not yet created by the Cloud Function");
       }

    });
}).catch(function(error) {
  var errorCode = error.code;
  var errorMessage = error.message;
  var email = error.email;
  var credential = error.credential;
  // ...
});

As said above, in the above code we only take into account the first Firestore document created by the addFirst() function. But you probably need to wait for the two docs to be created before reading them from the front-end.

So, you may modify you CF as follows:

export const createCollection = functions.auth.user().onCreate(async user => {
  try {
    await addFirst();
    await addSecond();
    return null;

    async function addFirst() {
      const userRef = admin.firestore().doc(`languages/${user.uid}`);
      await userRef.set(
        {
          language: null
        },
        { merge: true }
      );
    }

    async function addSecond() {
      // ...
    }

  } catch (error) {
    console.log(error);
    return null;
  }
});

Note that you don't need to use Promise.all(): the following two lines already execute the two document writes to Firestore. And, since you use async/await the second document is only written after the first one is written.

    const addLanguages = await addFirst();
    const addSecondCollection = await addSecond();

So you just need to set the listener on the path of the second document, and you are done!


Finally, note that doing

throw new functions.https.HttpsError('unknown', error);

in your catch block is the way you should handle errors for a Callable Cloud Function. Here, you are writing a background triggered Cloud Function, and you can just use return null;