1
votes

I'm trying to create a website that uses Firebase's databases to store user information. I want to use unique usernames. I have two indexes, one for users and another for usernames.

My database is structured like this:

users {
  $uid {
    username: "username1",
    gender: "xyz"
    email: "xyz"
  }
},
usernames {
    "username1": $uid"
}

The users claim a username with their $uid.

These are my rules:

{
  "rules": {
    "users": {
      "$uid": {
        ".write": "auth !== null && auth.uid === $uid",
        ".read": "auth !== null && auth.uid === $uid",
        "username": {
          ".validate": "
            !root.child('usernames').child(newData.val()).exists() ||
            root.child('usernames').child(newData.val()).val() == $uid"
        }
      }
    },
    "usernames" : {
      ".write": "!data.exists() && auth!= null",
      ".validate": "newData.val() == auth.uid"  <---- I can't get this too work
    }
  }
}

When setting username under $uid it checks the usernames index so username can only be written with a username not in use or one that has it's own $uid.

I only want data in which the value is the authenticated users uid and the key is the username. I can't quite get this to work. I suspect that I am using newData().val() incorrectly. My validate statement is failing.

I'd like to avoid using custom tokens, but I'm open to any suggestions. Thanks in advance.

Sorry if this explanation is too drawn out, this is my second post on StackOverflow.

Edit #2 I did some research from what I can tell and all I can find in docs talks about the need to use .child() before .val() but I need .child to take a variable instead of a set username.

2
You say that you cannot get it to work, but you don't say how it's not working. Does it fail or pass the validation regardless of the value?cartant
I'm with cartant here: at first glance the rules look good. What's failing? For something like this it helps to see the current JSON (as text, no screenshot) and the write operation that fails.Frank van Puffelen
Sorry about the lack of clarity. I'm currently failing on the validate on usernames.Michael Lai

2 Answers

0
votes

Sorry if i'm late but i ran into a similar problem, i changed my usernames rule to the following which did the trick:

"usernames" : {
  "$username": {
  ".write": "!data.exists() && auth!= null && newData.val() == auth.uid"
  }
 },
}
0
votes

The accepted answer will cause problems as soon as you want to allow people to change their usernames.

By slightly changing your database structure you can easily manage usernames in firebase. You can also allow users to change their username.

Database:

usernames: {
  username1: {
    owner: "userId1"
  },
  username2: {
    owner: "userId2"
  },
  ...
}

The following rules will:

  • stop any user changing/deleting another user's username
  • let a new user create a username
  • let a user change their username by deleting their old one and creating a new one

(split on newlines only for readability)

    "usernames": {
      "$username": {
        ".read": "true",
        ".write": "auth != null &&
          (
            \\ Allow user to delete only their own username
            (data.exists() && data.child('owner').val() === auth.uid && !newData.child('owner').exists())
            || 
            \\ Allow user to create a new username if the username is not taken
            (!data.exists() && newData.child('owner').val() === auth.uid)
          )"
      }
  }

This update will create a user:

  const toAdd = {}
  toAdd[`usernames/${username}`] = { owner: userId };
  firebase.database().ref().set(toAdd)

This update will change a users username:

const update = {}
update[`usernames/${previousUsername.toLowerCase()}`] = null;
update[`usernames/${newUsername.toLowerCase()}`] = { owner: userId };
firebase.database().ref().update(update)

In your code you should also query the database to check if a username exists before sending the update.

Why I avoided the username: userId approach:

You say

Edit #2 I did some research from what I can tell and all I can find in docs talks about the need to use .child() before .val() but I need .child to take a variable instead of a set username.

I believe this is correct. For your usernames setup:

usernames {
    "username1": userId1,
    "username2": userId2,
    ...
}

This answer describes a method which sends the key information in the request:

ref.update({
_username: "username3",
username3: "userId3",
})

Then you can check if the username is in the database with a rule like

data.child(newData.child('_username').val()).exists()

However in your usernames setup, you would then overwrite the other usernames with this update. To avoid that you would need to set the new data at the path of the username, usernames/username3 = userId3. But you cannot set it like this as you are then back at the problem of not having the key to reference in the rule.

You would need to create a nonsensical structure something like:

usernames: {
  username1: {
    _username: "username1",
    userId: "userId1"
  }  
}

So I chose the simpler, but unfortunately a slightly less beautiful username database setup I describe at the start.