chickenparm
chickenparm

Reputation: 1630

Firebase iOS Swift: Prevent Duplicate Usernames

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: "[email protected]"
            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)


          })

Upvotes: 0

Views: 1543

Answers (2)

chickenparm
chickenparm

Reputation: 1630

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)


              })

Upvotes: 0

Marcello Bastea-Forte
Marcello Bastea-Forte

Reputation: 1167

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.

Upvotes: 1

Related Questions