0

I am building an app that uses Firebase's email and password login feature. I am having the user register with a username, email, and password. I am struggling with how to stop the user from being created if the username is not unique. I have been reading other questions (specifically Firebase-android-make-username-unique and how-prevent-username-from-duplicate-signup-infirebase) but I have still not gotten it to fully work.

I followed the instructions in the first link above and set up my data structure as:

app : {
    users: {
       "some-user-uid": {
            email: "test@test.com"
            username: "myname"
       }
    },
    usernames: {
        "myname": "some-user-uid"
    }
}

and my security rules as:

"users": {
  "$uid": {
    ".write": "auth !== null && auth.uid === $uid",
    ".read": "auth !== null && auth.provider === 'password'",
    "username": {
      ".validate": "
        !root.child('usernames').child(newData.val()).exists() ||
        root.child('usernames').child(newData.val()).val() == $uid"
    }
  }
}

With this setup, if I try to create a new user with a username that already exists, it stops the user from being added to my data structure. When the below code is called, it prints "User Data could not be saved" if the username is a duplicate.

  func createNewAccount(uid: String, user: Dictionary<String, String>) {

    USER_REF.childByAppendingPath(uid).setValue(user, withCompletionBlock: {
      (error:NSError?, ref:Firebase!) in
      if (error != nil) {
        print("User Data could not be saved.")
      } else {
        print("User Data saved successfully!")
      }
    })
  }

  func addUsernameToUsernamePath (userData: Dictionary<String, String>) {

    USERNAME_REF.updateChildValues(userData)
  }

Here is where I am stuck. My create account method below doesn't call the above two methods until createUser and authUser are called (Which I need to get the uid). My problem is the user still gets created as a registered user and my security rules just keep the users information from being added to my data structure. I need to figure out how to stop the user from being created if there is a duplicate username.

@IBAction func createAccount() {
    let username = usernameField.text
    let email = emailField.text
    let password = passwordField.text

    if username != "" && email != "" && password != "" {

      // Set Email and Password for the New User.

      DataService.dataService.BASE_REF.createUser(email, password: password, withValueCompletionBlock: { error, result in

        if error != nil {
          print("Error: \(error)")
          if let errorCode = FAuthenticationError(rawValue: error.code) {
            switch (errorCode) {
            case .EmailTaken:
              self.signupErrorAlert("Email In Use", message: "An account has already been created for this email address.")
            default:
              self.signupErrorAlert("Oops!", message: "Having some trouble creating your account. Please try again or check your internet connection.")
            }
          }

        } else {

          DataService.dataService.BASE_REF.authUser(email, password: password, withCompletionBlock: {
            err, authData in


            let user = ["provider": authData.provider!, "email": email!, "username": username!]
            let userData = [username!: authData.uid!]

            DataService.dataService.createNewAccount(authData.uid, user: user)
            DataService.dataService.addUsernameToUsernamePath(userData)          

          })

EDIT

Here is my updated createAccount method that solved my issue.

  @IBAction func createAccount() {
    let username = usernameField.text
    let email = emailField.text
    let password = passwordField.text


if username != "" && email != "" && password != "" {

  DataService.dataService.USERNAME_REF.observeEventType(.Value, withBlock: { snapshot in
    var usernamesMatched = false
    if snapshot.value is NSNull {
      usernamesMatched = false
    } else {
      let usernameDictionary = snapshot.value
      let usernameArray = Array(usernameDictionary.allKeys as! [String])
      for storedUserName in usernameArray {
        if storedUserName == self.usernameField.text! {
          usernamesMatched = true
          self.signupErrorAlert("Username Already Taken", message: "Please try a different username")
        }
      }
    }

    if !usernamesMatched {
      // Set Email and Password for the New User.

      DataService.dataService.BASE_REF.createUser(email, password: password, withValueCompletionBlock: { error, result in

        if error != nil {
          print("Error: \(error)")
          if let errorCode = FAuthenticationError(rawValue: error.code) {
            switch (errorCode) {
            case .EmailTaken:
              self.signupErrorAlert("Email In Use", message: "An account has already been created for this email address.")
            default:
              self.signupErrorAlert("Oops!", message: "Having some trouble creating your account. Please try again or check your internet connection.")
            }
          }

        } else {

          // Create and Login the New User with authUser
          DataService.dataService.BASE_REF.authUser(email, password: password, withCompletionBlock: {
            err, authData in


            let user = ["provider": authData.provider!, "email": email!, "username": username!]
            let userData = [username!: authData.uid!]

            // Seal the deal in DataService.swift.
            DataService.dataService.createNewAccount(authData.uid, user: user)
            DataService.dataService.addUsernameToUsernamePath(userData)


          })
Community
  • 1
  • 1
chickenparm
  • 1,570
  • 1
  • 16
  • 36
  • Validate that the username doesn't exist before calling createUser()? – Kato May 10 '16 at 23:45
  • Thanks for the suggestion. I just updated my code and it appears to have solved the issue. – chickenparm May 11 '16 at 00:38
  • Why aren't you checking for the duplicate user user name first, via createUser->FAuthenticationErrorEmailTaken, and if it's available, then create the user in the /users node? If you do it that way you can reduce that code to just a few lines as that username is guaranteed not to already exist in your /users node. – Jay May 11 '16 at 18:09
  • That checks for a duplicate email address. createUser only has parameters for email and password. I am also creating a username for each user. In my application I have users enter a username, email, and password to create an account. I don't believe createUser has a way to check for a duplicate username. I did set up FAuthenticationErrorEmailTaken for checking duplicate email addresses. – chickenparm May 11 '16 at 18:27

2 Answers2

1

You could allow sign up without a valid username, and have a separate "set username" screen that you show in the event of a partial registration.

Define your security rules to check for a non-null username before allowing writes to other parts of your database.

Marcello Bastea-Forte
  • 1,167
  • 1
  • 10
  • 7
  • Thanks for the suggestion. I just found a way to solve it by checking if the username doesn't exist before calling createUser(). However, I appreciate the suggestion and may implement it if I am not happy with my solution. – chickenparm May 11 '16 at 00:39
  • You'll likely want to do both, otherwise you'll have a race condition if two people register at the same time for the same username. – Marcello Bastea-Forte May 11 '16 at 17:50
  • Good point. I just tried creating two accounts with the same username on two iPhones. When I touched create account at the same exact time I ran into issues. – chickenparm May 11 '16 at 18:29
0

I was able to get it working by updating createAccount() to the code below.

  @IBAction func createAccount() {
    let username = usernameField.text
    let email = emailField.text
    let password = passwordField.text


    if username != "" && email != "" && password != "" {

      // Checks for internet connection before saving the meetup. Returns if there is no internet connection.
      let reachability = try! Reachability.reachabilityForInternetConnection()

      if reachability.currentReachabilityStatus == .NotReachable {
        let internetAlert = UIAlertController(title: "No Internet Connection", message: "Please make sure your device is connected to the internet.", preferredStyle: .Alert)
        let internetAlertAction = UIAlertAction(title: "OK", style: .Default, handler: nil)
        internetAlert.addAction(internetAlertAction)
        presentViewController(internetAlert, animated: true, completion: nil)
        return
      }
      DataService.dataService.USERNAME_REF.observeEventType(.Value, withBlock: { snapshot in
        var usernamesMatched = false
        if snapshot.value is NSNull {
          usernamesMatched = false
        } else {
          let usernameDictionary = snapshot.value
          let usernameArray = Array(usernameDictionary.allKeys as! [String])
          for storedUserName in usernameArray {
            if storedUserName == self.usernameField.text! {
              usernamesMatched = true
              self.signupErrorAlert("Username Already Taken", message: "Please try a different username")
            }
          }
        }

        if !usernamesMatched {
          // Set Email and Password for the New User.

          DataService.dataService.BASE_REF.createUser(email, password: password, withValueCompletionBlock: { error, result in

            if error != nil {
              print("Error: \(error)")
              if let errorCode = FAuthenticationError(rawValue: error.code) {
                switch (errorCode) {
                case .EmailTaken:
                  self.signupErrorAlert("Email In Use", message: "An account has already been created for this email address.")
                default:
                  self.signupErrorAlert("Oops!", message: "Having some trouble creating your account. Please try again or check your internet connection.")
                }
              }

            } else {

              // Create and Login the New User with authUser
              DataService.dataService.BASE_REF.authUser(email, password: password, withCompletionBlock: {
                err, authData in


                let user = ["provider": authData.provider!, "email": email!, "username": username!]
                let userData = [username!: authData.uid!]

                // Seal the deal in DataService.swift.
                DataService.dataService.createNewAccount(authData.uid, user: user)
                DataService.dataService.addUsernameToUsernamePath(userData)


              })
chickenparm
  • 1,570
  • 1
  • 16
  • 36