How do you prevent duplicate user properties in Firebase?

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.