node.jsfirebasegoogle-cloud-firestoregoogle-cloud-functions

Skipping write to Firestore if Anonymous user logic


So this use case is pretty straightforward: I have a couple of sign-in providers activated in my Firebase web app as you can see in the picture.

Mostly my sign-in and sign-up logic is handled in the front-end with vanilla JS, I have 5 different scripts, one for each SSO provider.

Now why I did like this is another topic, but I fetch and store different types of user data depending on which social media provider they signed in with. This is also because the APIs returns different parameters.

Anyhow, everything works flawless when a user follows the on boarding that its setup to have in my web app when they sign-up, but if they bypass it and goes straight to sign-in page and authenticate with a SSO provider from there, the data in Firestore data is not stored properly, instead, my Cloud Functions I have created to prevent update/write for Anonymous users is triggered and that node.js script is triggered and overriding the front-end instead.

Now I want to handle this from the front-end because I am storing data in localStorage that I need to use (like i18n language selection etc) among other things.

My question is this: How do I keep a Cloud Function that only prevents Anonymous users data to be written in Firestore? There seem to not be enough to only have a simple function that checks the providers length and return null. So any idea would be appreciated!


Solution

  • In your current logic, you are calling a signInWith method, then after it succeeds, you are creating the required Firestore document. But you also don't appear to be doing this in all places where a sign in is possible in your application.

    Care should be taken to always handle the result of the Auth#signInWith and User#linkWith calls across your application in a consistent way. To help with this, we are going to create a handleUserCredential callback that processes the result of these sign in and link requests with the same logic. If the handled credential represents a new user, or there is new profile information available because a new account is linked, we want to update the user's data in their Firestore document.

    // Note: using legacy/compat (namespaced) syntax to match question. Use modern (modular) syntax where possible in new projects.
    
    const handleUserCredential = async (userCredential /* firebase.auth.UserCredential */) => {
      // extract useful information from userCredential
      const credentialProviderId = userCredential.credential.providerId;
      const user = userCredential.user;
      const providerUserInfo = user.providerData
        .find(userInfo => userInfo.providerId ===  credentialProviderId);
      const userDocRef = firebase.firestore()
        .collection("users")
        .doc(user.uid);
    
      // define helper function for async errors.
      const rethrowLinkedError = (err) => {
        // attach credential for when an error handler may need it
        try {
          err.userCredential = userCredential;
        } catch (attachError) {
          console.error("Could not attach credential to thrown error.", attachError);
        }
        throw err;
      }
    
      // treat as new user if isNewUser flag is true, or when that
      // user's data does not exist in Firestore
      const isNewUser = userCredential.additionalUserInfo.isNewUser
        || !((await userDocRef.get().catch(rethrowLinkedError)).exists);
    
      if (!isNewUser && userCredential.operationType !== 'link') {
        // user is not new, missing data, nor is linking an account.
        // return early while passing userCredential to next handler
        return userCredential;
      }
    
      // if here, the signed in user is new, their Firestore data is
      // missing, or a new sign in method was just linked to an
      // existing account that may need its profile updated.
    
      // scaffold out the base document data from the User object
      // we'll override these below as needed
      const userDocData = {
         uid: user.uid,
         email: user.email,
         displayName: user.displayName,
         phone: user.phoneNumber,
         picture: user.photoURL, // recommend using photoURL for consistency
      }
    
      const langPreference = localStorage.getItem('preferredLanguage');
      if (langPreference || isNewUser) {
        // only update language for new users or when a preference is set.
        // otherwise, leave setting as-is on Firestore
        userDocData.lang = langPreference;
      }
    
      // override user data based on the newly added provider
      switch (credentialProviderId) {
        case 'google.com': // GoogleAuthProvider.PROVIDER_ID
          if (!userDocData.picture && providerUserInfo.photoURL) {
            // use Google's provided photo when one was not already set.
            // the "if not already set" check can be removed as desired.
            
            // Remove the size parameter
            userDocData.picture = providerUserInfo.photoURL.replace(/=\s*s\d+-c/, '');
          }
          break;
        case 'facebook.com':
          // ...
          break;
        // ... other handlers
      }
    
      await userDocRef
        .set(userDocData, { merge: true })
        .catch(rethrowLinkedError);
    
      // consider also adding:
      // await user.updateProfile({
      //   displayName: userDocData.displayName,
      //   photoURL: userDocData.picture
      // })
      //   .catch(rethrowLinkedError);
    
      return userCredential;
    }
    

    Note: In the above code, I removed lang from the base user data document. This was so that when a preference does not exist in local storage, you don't inadvertently remove their preference stored in Firestore.


    The above function can then be chained to the result of the Auth#signInWith* or User#linkWith* methods.

    // Note: using legacy/compat (namespaced) syntax to match question. Use modern (modular) syntax where possible in new projects.
    
    firebase.auth()
      .signInWithPopup(provider)
      .then(handleUserCredential) // <-- handles the result and passes it on
      .then((result) => {
        // if here, user is now signed in, and their Firestore document was updated as needed.
        
        setTimeout(() => window.location.href = "../pages/home.html", 1000);
      })
      .catch(err => {
        switch (err.code || err.message) {
          case "auth/user-disabled": // example Authentication error handling
            // TODO: handle this error
            break;
          case "permission-denied": // example Firestore error handling
            // TODO: handle this error
            break;
          default:
            if (err.code && err.code.startsWith("auth/")) {
              // handle general auth error
            } else if (err.code) {
              // handle other error, likely, but not guaranteed, to be related to Firestore
            } else {
              // handle other unexpected error
            }
        }
      })
    
    // Note: using legacy/compat (namespaced) syntax to match question. Use modern (modular) syntax where possible in new projects.
    
    const auth = firebase.auth();
    
    auth.currentUser
      .linkWithPopup(provider)
      .catch(err => {
        // if here, the link operation failed. Determine what to do with
        // the credential that could not be linked.
        if (err.code === "auth/credential-already-in-use") {
          // sign in with the other account instead? cancel? error?
          return auth.signInWithCredential(err.credential);
        }
        throw err; // rethrow other errors
      })
      .then(handleUserCredential) // <-- handles the result and passes it on
      .then((result) => {
        // if here, user is now signed in, and their Firestore document
        // was updated as needed.
        
        // if result.operationType === 'link', the accounts were linked.
        // if result.operationType === 'signIn', the link failed and the user
        // was instead signed in directly with that account.
        
        setTimeout(() => window.location.href = "../pages/home.html", 1000);
      })
      .catch(err => {
        // TODO: Handle errors as above.
      })
    

    Potential errors: