How do you prevent duplicate user properties in Firebase?

16,550

Solution 1

First of all, if users already have a username, it's unique, and this is not going to go away, I'd recommend that you give up on using simple login uids. This is going to create nothing but issues trying to flip back and forth between the two, as you've already discovered here. Investigate creating your own tokens with a tool like firebase-passport-login and then store the records by username.

But since that wasn't your question, let's resolve that while we're here, since you may want to go ahead and enter the thorny briar of dual identities through which I have passed many times.

To make the username unique, store an index of usernames.

/users/$userid/username/$username
/usernames/$username/$userid

To ensure they are unique, add a security rule as follows on the user id in usernames/ path, which ensures only one user can be assigned per username and that the value is the user's id:

".write": "newData.val() === auth.uid && !data.exists()"

Now enforce that they match by adding the following to the username in the users/ record:

"users": {
   "$userid": {
      "username": {
         ".validate": "root.child('usernames/'+newData.val()).val() === $userid"
      }
   }
}

This will ensure the ids are unique. Be careful with read privileges. You may want to avoid those entirely since you don't want anyone looking up private emails or usernames. Something like I demonstrated in support for saving these would be ideal.

The idea here is that you try to assign the username and email, if they fail, then they already exist and belong to another user. Otherwise, you insert them into the user record and now have users indexed by uid and email.

To comply with SO protocol, here's the code from that gist, which is better read via the link:

var fb = new Firebase(URL);

function escapeEmail(email) {
   return email.replace('.', ',');
}

function claimEmail(userId, email, next) {
   fb.child('email_lookup').child(escapeEmail(email)).set(userId, function(err) {
      if( err ) { throw new Error('email already taken'); }
      next();
   });
}

function claimUsername(userId, username, next) {
   fb.child('username_lookup').child(username).set(userId, function(err) {
      if( err ) { throw new Error('username already taken'); }
      next();
   });   
}

function createUser(userId, data) {
   claimEmail(userId, data.email, claimUsername.bind(null, userId, data.username, function() {
      fb.child('users').child(userId).set(data);
   );   
}

And the rules:

{
  "rules": {
     "users": {
        "$user": {
           "username": {
               ".validate": "root.child('username_lookup/'+newData.val()).val() === auth.uid"
           },
           "email": {
               ".validate": "root.child('email_lookup').child(newData.val().replace('.', ',')).val() === auth.uid"
           }
        }
     },

     "email_lookup": {
        "$email": {
           // not readable, cannot get a list of emails!
           // can only write if this email is not already in the db
           ".write": "!data.exists()",
           // can only write my own uid into this index
           ".validate": "newData.val() === auth.uid"
        }
     },
     "username_lookup": {
        "$username": {
           // not readable, cannot get a list of usernames!
           // can only write if this username is not already in the db
           ".write": "!data.exists()",

           // can only write my own uid into this index
           ".validate": "newData.val() === auth.uid"
        }
     },
  }
}

Solution 2

Wouldn't it be easier to just use the security rules to check for it's existence? I have mine set up as follows:

"usernames": {
  "$usernameid": {
    ".read": "auth != null",
        ".write": "auth != null  && (!data.exists() || !newData.exists())"
    }
    }

This allows the write if the username doesn't exist. I believe I got this directly from the Firebase docs.

Share:
16,550
reknirt
Author by

reknirt

Updated on June 02, 2022

Comments

  • reknirt
    reknirt about 2 years

    I'm Using FirebaseSimpleLogin to create users and handle authentication.

    When I try and create a new user with simple login via the $createUser() method, firebase won't create the user if the email address has already been used. However, I am also using $set() to save my created users to my firebase after I create them and I am using user.uid as the key. When trying to write to the database, firebase will save the record even if the username is not unique since only email and password are required for simple login. So how can I validate a username to be unique when it is not being used as the key to the user object?

    I'm creating new users like this:

    $scope.createUser = function() {
      $scope.auth.$createUser('[email protected]', 'password').then(function(user, err) {
        if (!err) {
          ref.child('users/' + user.uid).set({
            email: user.email,
            username: user.username
          });
          console.log("success!");
        }else{
          console.log(err.message);
        }
      });
    }
    

    And my user object looks like this:

    {
      "users" : {
        "simplelogin:28" : {
          "email" : "[email protected]",
          "username" : "jtrinker"
        },
        "simplelogin:30" : {
          "email" : "[email protected]",
          "username" : "jtrinker"
        }
      }
    }
    
    }
    

    I need to use the uid as the key for each user, but I still need the username to be unique.

    How can I can I prevent firebase from saving records if properties within one object are not unique to properties inside a different object?