10

I have recently followed a tutorial over on Thinkster for creating a web app using Angular and Firebase.

The tutorial uses the Firebase simpleLogin method allows a 'profile' to be created that includes a username.

Factory:

app.factory('Auth', function($firebaseSimpleLogin, $firebase, FIREBASE_URL, $rootScope) {
var ref = new Firebase(FIREBASE_URL);


var auth = $firebaseSimpleLogin(ref);

var Auth = {
    register: function(user) {
       return auth.$createUser(user.email, user.password);
    },
    createProfile: function(user) {
        var profile = {
            username: user.username,
            md5_hash: user.md5_hash
        };

        var profileRef = $firebase(ref.child('profile'));
        return profileRef.$set(user.uid, profile);
    },
    login: function(user) {
        return auth.$login('password', user);
    },
    logout: function() {
        auth.$logout();
    },
    resolveUser: function() {
        return auth.$getCurrentUser();
    },
    signedIn: function() {
        return !!Auth.user.provider;
    },
    user: {}
};

$rootScope.$on('$firebaseSimpleLogin:login', function(e, user) {
    angular.copy(user, Auth.user);
    Auth.user.profile = $firebase(ref.child('profile').child(Auth.user.uid)).$asObject();

    console.log(Auth.user);
});
$rootScope.$on('$firebaseSimpleLogin:logout', function() {
    console.log('logged out');

    if (Auth.user && Auth.user.profile) {
        Auth.user.profile.$destroy();
    }
    angular.copy({}, Auth.user);
});

return Auth;
});

Controller:

$scope.register = function() {
    Auth.register($scope.user).then(function(user) {
        return Auth.login($scope.user).then(function() {
            user.username = $scope.user.username;
            return Auth.createProfile(user);
        }).then(function() {
            $location.path('/');
        });
    }, function(error) {
        $scope.error = error.toString();
    });
};

At the very end of the tutorial there is a 'next steps' section which includes:

Enforce username uniqueness-- this one is tricky, check out Firebase priorities and see if you can use them to query user profiles by username

I have searched and searched but can't find a clear explanation of how to do this, particularly in terms of the setPriority() function of Firebase

I'm quite the Firebase newbie so any help here would be gratefully recieved.

There are a few similar questions, but I can't seem to get my head around how to sort this out.

Enormous thanks in advance.


EDIT

From Marein's answer I have updated the register function in my controller to:

$scope.register = function() {

    var ref = new Firebase(FIREBASE_URL);
    var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);
    q.once('value', function(snapshot) {
        if (snapshot.val() === null) {
            Auth.register($scope.user).then(function(user) {
                return Auth.login($scope.user).then(function() {
                    user.username = $scope.user.username;
                    return Auth.createProfile(user);
                }).then(function() {
                    $location.path('/');
                });
            }, function(error) {
                $scope.error = error.toString();
            });
        } else {
            // username already exists, ask user for a different name
        }
    });
};

But it is throwing an 'undefined is not a function' error in the line var q = ref.child('profile').orderByChild('username').equalTo($scope.user.username);. I have commented out the code after and tried just console.log(q) but still no joy.

EDIT 2

The issue with the above was that the Thinkster tutorial uses Firebase 0.8 and orderByChild is available only in later versions. Updated and Marein's answer is perfect.

Community
  • 1
  • 1
Nick Moreton
  • 228
  • 2
  • 9
  • 1
    I don't understand how the tutorial means to use priorities with this, perhaps they meant [queries](https://www.firebase.com/docs/web/api/query/). Those may be used to check client-side whether a user with a certain username already exists. Additionally you may want to look into server-side security rules, to prevent writing usernames that already exist. –  Apr 30 '15 at 14:51
  • 1
    Good point Marein, I think that part of the thinkster tutorial might be a left-over from when before Firebase supported `orderByChild`. Back then all you had was `setPriority`, so that would have been the only querying mechanism. Nowadays I would indeed simply store the users `email` in their `/profile/` node and then `ref.child('profile').orderByChild('email').equalTo(emailThatIsTryingToRegister).once('value'...` – Frank van Puffelen Apr 30 '15 at 16:05
  • Thank you both @FrankvanPuffelen - I think I understand this conceptually, but would you mind giving me some guidance as to where and how this would fit in to my existing code? Also, I presume that in my particular use case I would be using `orderByChild('username')`? – Nick Moreton Apr 30 '15 at 16:22

2 Answers2

19

There are two things to do here, a client-side check and a server-side rule.

At the client side, you want to check whether the username already exists, so that you can tell the user that their input is invalid, before sending it to the server. Where exactly you implement this up to you, but the code would look something like this:

var ref = new Firebase('https://YourFirebase.firebaseio.com');
var q = ref.child('profiles').orderByChild('username').equalTo(newUsername);
q.once('value', function(snapshot) {
  if (snapshot.val() === null) {
    // username does not yet exist, go ahead and add new user
  } else {
    // username already exists, ask user for a different name
  }
});

You can use this to check before writing to the server. However, what if a user is malicious and decides to use the JS console to write to the server anyway? To prevent this you need server-side security.

I tried to come up with an example solution but I ran into a problem. Hopefully someone more knowledgeable will come along. My problem is as follows. Let's say your database structure looks like this:

{
  "profiles" : {
    "profile1" : {
      "username" : "Nick",
      "md5_hash" : "..."
    },
    "profile2" : {
      "username" : "Marein",
      "md5_hash" : "..."
    }
  }
}

When adding a new profile, you'd want to have a rule ensuring that no profile object with the same username property exists. However, as far as I know the Firebase security language does not support this, with this data structure.

A solution would be to change the datastructure to use username as the key for each profile (instead of profile1, profile2, ...). That way there can only ever be one object with that username, automatically. Database structure would be:

{
  "profiles" : {
    "Nick" : {
      "md5_hash" : "..."
    },
    "Marein" : {
      "md5_hash" : "..."
    }
  }
}

This might be a viable solution in this case. However, what if not only the username, but for example also the email has to be unique? They can't both be the object key (unless we use string concatenation...).

One more thing that comes to mind is to, in addition to the list of profiles, keep a separate list of usernames and a separate list of emails as well. Then those can be used easily in security rules to check whether the given username and email already exist. The rules would look something like this:

{
  "rules" : {
    ".write" : true,
    ".read" : true,
    "profiles" : {
      "$profile" : {
        "username" : {
          ".validate" : "!root.child('usernames').child(newData.val()).exists()"
        }
      }
    },
    "usernames" : {
      "$username" : {
        ".validate" : "newData.isString()"
      }
    }
  }
}

However now we run into another problem; how to ensure that when a new profile is created, the username (and email) are also placed into these new lists? [1]

This in turn can be solved by taking the profile creation code out of the client and placing it on a server instead. The client would then need to ask the server to create a new profile, and the server would ensure that all the necessary tasks are executed.

However, it seems we have gone very far down a hole to answer this question. Perhaps I have overlooked something and things are simpler than they seem. Any thoughts are appreciated.

Also, apologies if this answer is more like a question than an answer, I'm new to SO and not sure yet what is appropriate as an answer.

[1] Although maybe you could argue that this does not need to be ensured, as a malicious user would only harm themselves by not claiming their unique identity?

Paulo Mattos
  • 18,845
  • 10
  • 77
  • 85
  • 1
    Thanks Frank! Though I feel it's a bit open-ended. Do you maybe have anything to add? –  May 01 '15 at 16:24
  • Thank you @Marein! I actually think the initial solution will work as my Firebase rules only allow for the 'profiles' data to be written if a user is authorised - the Firebase simplelogin handles the email uniqueness separately from my database. As it stands the profile is written once the user has been created. I will test the code out tonight when I get to a computer and report back! Thanks so much for such a thorough response – Nick Moreton May 01 '15 at 17:57
  • Glad I could help! If you are certain that authorised users are not malicious, then just doing a client-side check is enough. –  May 01 '15 at 18:01
  • Hi @marein Just trying this now but `var q = ref.child('profiles').orderByChild('username').equalTo(newUsername);` is throwing an 'undefined is not a function' error. I wondered if it was because username was one level deep in the data (so it goes 'profiles' > '' > 'username'), but looking at the docs the [orderByChild](https://www.firebase.com/docs/web/api/query/orderbychild.html) example they use follows the same structure. Any ideas? – Nick Moreton May 01 '15 at 21:19
  • Hi Nick, the code is not throwing that error for me. It seems most likely that you made a typo in one of the function calls (`child`, ...) or that `ref` doesn't refer to a Firebase reference. Perhaps you could edit your question with additional code? –  May 01 '15 at 21:28
  • Thanks @Marein - I have added the altered code in the question - I also ensured that I included $firebase in the controller dependencies too (something I had originally missed!) – Nick Moreton May 01 '15 at 21:52
  • Hmm... the error seems to be in `Auth`. but I'd have to set up an Angular app to investigate further. In the meantime, the `$firebaseSimpleLogin` module is deprecated; you might want to switch to [`$firebaseAuth`](https://www.firebase.com/docs/web/libraries/angular/guide/user-auth.html), and it might solve the error along the way. –  May 01 '15 at 22:21
  • @Marein thanks - I would be surprised if it was Auth as that has been working before I attempted the username check, and I also commented out all the subsequent Auth stuff in that register function just in case. I will happily share any and all code I have with you to investigate - I'm very grateful for your help! – Nick Moreton May 01 '15 at 22:30
  • I have just thought - the tutorial I followed used an old version of Firebase (0.8 I think) - and I think @FrankvanPuffelen mentioned that orderByChild is a new feature. I am an idiot. I will check in the morning as I bet this is the issue! Sorry for wasting your time! – Nick Moreton May 01 '15 at 22:33
  • Ah, indeed that must be it! No problem at all, I hope you'll get it working in the morning. You can find the latest versions [here](https://www.firebase.com/docs/web/libraries/angular/guide/intro-to-angularfire.html#section-getting-started). I think using these will break `$firebaseSimpleLogin`, like I mentioned. –  May 01 '15 at 22:34
  • 1
    @Marein - yes this was indeed the issue! I'll now go through the app and update all the deprecated functions. Thanks so much! – Nick Moreton May 02 '15 at 06:37
-1

I had a similar problem. But it was after registering the user with password and email. In the user profile could save a user name that must be unique and I have found a solution, maybe this can serve you.

Query for username unique in Firebase

        var ref = new Firebase(FIREBASE_URL + '/users');

        ref.orderByChild("username").equalTo(profile.username).on("child_added", function(snapshot) {
            if (currentUser != snapshot.key()) {
                scope.used = true;
            }   
        }); 

        ref.orderByChild("username").equalTo(profile.username).once("value", function(snap) {
            //console.log("initial data loaded!", Object.keys(snap.val()).length === count);
            if (scope.used) {
                console.log('username already exists');
                scope.used = false;
            }else{
                console.log('username doesnt exists, update it');
                userRef.child('username').set(profile.username);
            }   
        }); 
    };  
Community
  • 1
  • 1
Lorraine
  • 441
  • 5
  • 23