Good way to replace invalid characters in firebase keys? - firebase

My use case is saving a user's info. When I try to save data to Firebase using the user's email address as a key, Firebase throws the following error:
Error: Invalid key e#e.ee (cannot contain .$[]#)
So, apparently, I cannot index user info by their email. What is the best practice to replace the .?
I've had success changing the . to a - but that won't cut it since some email's have -s in the address.
Currently, I'm using
var cleanEmail = email.replace('.','`');
but there are likely going to be conflicts down the line with this.

In the email address, replace the dot . with a comma ,. This pattern is best practice.
The comma , is not an allowable character in email addresses but it is allowable in a Firebase key. Symmetrically, the dot . is an allowable character in email addresses but it is not allowable in a Firebase key. So direct substitution will solve your problem. You can index email addresses without looping.
You also have another issue.
const cleanEmail = email.replace('.',','); // only replaces first dot
will only replace the first dot . But email addresses can have multiple dots. To replace all the dots, use a regular expression.
const cleanEmail = email.replace(/\./g, ','); // replaces all dots
Or alternatively, you could also use the split() - join() pattern to replace all dots.
const cleanEmail = email.split('.').join(','); // also replaces all dots

We've dealt with this issue many times and while on the surface it seems like using an email as a key is a simple solution, it leads to a lot of other issues: having to clean/parse the email so it can actually be used. What if the email changes?
We have found that changing the format of how the data is stored is a better path. Suppose you just need to store one thing, the user name.
john#somecompany.com: "John Smith"
changing it to
randomly_generated_node_name
email: "john#somecompany.com"
first: "John"
last: "Smith"
The randomly_generated_node_name is a string that Firebase can generate via childByAutoId, or really any type of reference that is not tied directly to the data.
This offers a lot of flexibility: you can now change the persons last name - say if they get married. Or change their email. You could add an 'index' child 0, 1, 2 etc that could be used for sorting. The data can be queried for any child data. All because the randomly_generated_node_name is a static reference to the variable child data within the node.
It also allows you to expand the data in the future without altering the existing data. Add address, favorite food, an index for sorting etc.
Edit: a Firebase query for email in ObjC:
//references all of the users ordered by email
FQuery *allUsers = [myUsersRef queryOrderedByChild:#"email"];
//ref the user with this email
FQuery *thisSpecificUser = [allUsers queryEqualToValue:#“john#somecompany.com”];
//load the user with this email
[thisSpecificUser observeEventType:FEventTypeChildAdded withBlock:^(FDataSnapshot *snapshot) {
//do something with this user
}];

I can think of two major ways to solve this issue:
Encode/Decode function
Because of the limited set of characters allowed in a Firebase key, a solution is to transform the key into an valid format (encode). Then have an inverse function (decode) to transform the encoded key back as the original key.
A general encode/decode function might be transforming the original key into bytes, then converting them to a hexadecimal representation. But the size of the key might be an issue.
Let's say you want to store users using the e-mail as key:
# path: /users/{email} is User;
/users/alice#email.com: {
name: "Alice",
email: "alice#email.com"
}
The example above doesn't work because of the dot in the path. So we use the encode function to transform the key into a valid format. alice#email.com in hexadecimal is 616c69636540656d61696c2e636f6d, then:
# path: /users/{hex(email)} is User;
/users/616c69636540656d61696c2e636f6d: {
name: "Alice",
email: "alice#email.com"
}
Any client can access that resource as long as they share the same hex function.
Edit: Base64 can also be used to encode/decode the key. May be more efficient than hexadecimals, but there are many different implementations. If clients doesn't share the exact same implementation, then they will not work properly.
Specialized functions (ex. that handles e-mails only) can also be used. But be sure to handle all the edge cases.
Encode function with original key stored
Doing one way transformation of the key is a lot easier. So, instead of using a decode function, just store the original key in the database.
A good encode function for this case is the SHA-256 algorithm. It's a common algorithm with implementations in many platforms. And the chances of collisions are very slim.
The previous example with SHA-256 becomes like this:
# path: /users/{sha256(email)} is User;
/users/55bf4952e2308638427d0c28891b31b8cd3a88d1610b81f0a605da25fd9c351a: {
name: "Alice",
email: "alice#email.com"
}
Any client with the original key (the e-mail) can find this entry, because the encode function is known (it is known). And, even if the key gets bigger, the size of the SHA-256 will always be the same, therefore, guaranteed to be a valid Firebase key.

I am using the following code for converting email to hash and then using the hash as key in firebase
public class HashingUtils {
public HashingUtils() {
}
//generate 256 bits hash using SHA-256
public String generateHashkeySHA_256(String email){
String result = null;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-256");
byte[] hash = digest.digest(email.getBytes("UTF-8"));
return byteToHex(hash); // make it printable
}catch(Exception ex) {
ex.printStackTrace();
}
return result;
}
//generate 160bits hash using SHA-1
public String generateHashkeySHA_1(String email){
String result = null;
try {
MessageDigest digest = MessageDigest.getInstance("SHA-1");
byte[] hash = digest.digest(email.getBytes("UTF-8"));
return byteToHex(hash); // make it printable
}catch(Exception ex) {
ex.printStackTrace();
}
return result;
}
public String byteToHex(byte[] bytes) {
Formatter formatter = new Formatter();
for (byte b : bytes) {
formatter.format("%02x", b);
}
String hex = formatter.toString();
return hex;
}
}
code for adding the user to firebase
public void addUser(User user) {
Log.d(TAG, "addUser: ");
DatabaseReference userRef= database.getReference("User");
if(!TextUtils.isEmpty(user.getEmailId())){
String hashEmailId= hashingUtils.generateHashkeySHA_256(user.getEmailId());
Log.d(TAG, "addUser: hashEmailId"+hashEmailId);
userRef.child(hashEmailId).setValue(user);
}
else {
Log.d(TAG,"addUser: empty emailId");
}
}

Related

Objectify Web Safe Key Usage

I am using Objectify to store and retrieve data from App Engine Datastore.
String version of the key is created from the datastore object id.
public String getWebsafeKey() {
return Key.create(UserData.class, id).getString();
}
The websafeKey is used to get the UserData object from the Datastore.
Key<UserData> userDataKey = Key.create(websafeKey);
UserData userData = ofy().load().key(userDataKey).now();
In our Unit testing when the websafeKey is changed a bit, the user data class can still be retrieved.
Passed websafeKey - agxqfmMyaHF1YWxpdHlyEgsSBU1vdmllGICAgJDSioELDC
Actual websafeKey - agxqfmMyaHF1YWxpdHlyEgsSBU1vdmllGICAgJDSioELDA
Is this a known limitation or this can be addressed?
websafeKey's are base64 encoded strings.
Somehow both
agxqfmMyaHF1YWxpdHlyEgsSBU1vdmllGICAgJDSioELDC &
agxqfmMyaHF1YWxpdHlyEgsSBU1vdmllGICAgJDSioELDA decode to jj~c2hqualityrMovie
give it a try https://www.base64decode.org/

How to avoid store same values on DB

Considering my firebase DB:
How I ensure the email (or other field) uniqueness at firebase side (using rules)?
Thank you
I suggest you in stead of using the unique id generated by the push() method, to use the email address. Because Firebase does not accept symbols as . in the key, you'll need to encode the email address like this:
name#email.com -> name#email,com
As you probably see, the . symbol is changed with ,. To encode and decode the email address, i suggest you use this methods:
static String encodeUserEmail(String userEmail) {
return userEmail.replace(".", ",");
}
static String decodeUserEmail(String userEmail) {
return userEmail.replace(",", ".");
}
Than to verify the uniqueness of your users, just put a listener on the users node and use the exists() method on the dataSnapshot.
Hope it helps.

How to parse a collection's sub-object to find a unique result from many possibilities?

In my user's schema, I have a TokAuth Array with token sub-objects (like multiple mails addresses).
So in a method, when I search the tokens in the current user :
var id = Meteor.userId();
var usercurrent = Meteor.users.findOne({_id: id}, {fields: {"TokAuth": 1}});
var userToken = usercurrent.TokAuth.token;
I got in console.log(userToken)
[ 'fyAyXkXYrQdAlNpjuQfJ8RLU2TpfVGLnptlBs-m1h7xk',
I20170224-20:36:23.202(1)? 'YTwtUbhNTgiEfzFbJq7mESnOoOHeLYxWlqEeJJIG_GiV',
I20170224-20:36:23.206(1)? 'ViA4ydDITJtHDi2c_sArkNtpRYTjFqGL1ju2v00_-rFJ',
I20170224-20:36:23.206(1)? '51ImZcxRADLJr-FPCUL7EFGnTZYjHSZk3XxdqtBV2_fd',
I20170224-20:36:23.207(1)? 'S5aEvqjJ5zTUJqLFCPY1aZ1ZhsQppZTJtYKULM9aS2B3',
I20170224-20:36:23.207(1)? 'mhBs3oxHf2SxZfu2vCZhtiyPfg25fKMY8bKMZD8fx6IG',
I20170224-20:36:23.207(1)? '-rv0FiP-lxoqe8INyCJASV6rZpbgy3euEqB9sO9HsZSV',
I20170224-20:36:23.207(1)? 'zacr6_VBjHTsArov1LmQyZFLwI40fx4J7sygpLosTrli' ]
Beside, I've got a var who is equal to the last token in the userToken sub-object (that's of course expected : not to be the last one, but to be in the sub-object).
console.log (editAuth);
zacr6_VBjHTsArov1LmQyZFLwI40fx4J7sygpLosTrli
So how can I parse userToken to find a match with editAuth? If userToken was just a String, it will be simple but here...
Thanks
Is there a reason you are storing all the tokens as an array as opposed to just updating a single string each time?
That aside, you can check if an array contains a value by using the handy underscore function _.contains
Example:
_.contains( userToken, editAuth ); //returns true or false
In this case, you are simply trying to search for a string within an array of strings. #Sean already provided one solution.
If you are using the meteor ecmascript package then you can just simply use the native Array.includes method.
userToken.includes(editAuth);
On a side note, after using ECMAScript 2015+ for some time now, I find that I can use the native API for almost everything that I used to use underscore or lodash for. Check it out!

Firebase security rules to check unique value of a child #AskFirebase

The structure of my firebase database is as shown above. How do I ensure the urls are unique and there are no duplicates? Since, these are urls I cannot use them directly as paths, I am forced to use them as values. So solutions like this won't work.
If you want something to be unique within the Firebase Database, you should store it as a key. That automatically guarantees uniqueness.
As you noted, some characters cannot be used in a key. In that case you'll need to encode the value to allow it in a key and ensure you don't lose the information that makes the value unique. A very simple case of this is when someone wants to store a unique email address in the database. Since a key cannot contain . characters, we need to encode that. A common encoding for this is to replace the . with a ,:
users: {
"uidOfPuf": {
name: "Frank van Puffelen",
email: "puf#firebaseui.com"
}
},
emailAddresses: {
"puf#firebaseui,com": "uidOfPuf"
}
Using a , is especially handy when it comes to email addresses, since an email address cannot contain a ,.
But in general all that matters is that the encoded value is "reasonably guaranteed to be unique" and that you still store the actual value somewhere too (such as the /users/$uid/email above).
For encoding URLs, I'd simply start with stripping all illegal characters:
var url = "http://stackoverflow.com/questions/39149216/firebase-security-rules-to-check-unique-value-of-a-child-askfirebase";
ref.child(url.replace(/[\.\/]/g, '')).set(url);
Stores:
"http:stackoverflowcomquestions39149216firebase-security-rules-to-check-unique-value-of-a-child-askfirebase": "http://stackoverflow.com/questions/39149216/firebase-security-rules-to-check-unique-value-of-a-child-askfirebase"
Update: I'm considering if using a simply hashcode for the key, which leads to more reasonably length keys:
// from http://stackoverflow.com/questions/7616461/generate-a-hash-from-string-in-javascript-jquery
String.prototype.hashCode = function(){
var hash = 0;
if (this.length == 0) return hash;
for (i = 0; i < this.length; i++) {
char = this.charCodeAt(i);
hash = ((hash<<5)-hash)+char;
hash = hash & hash; // Convert to 32bit integer
}
return hash;
}
var url = "http://stackoverflow.com/questions/39149216/firebase-security-rules-to-check-unique-value-of-a-child-askfirebase";
ref.child(url.hashCode()).set(url);
Leads to:
20397229: "http://stackoverflow.com/questions/39149216/firebase-security-rules-to-check-unique-value-of-a-child-askfirebase"

GitKit Client - Uploaded users cannot connect

We have an existing user database with SHA1-encoded passwords. We upload them to the Google Federated Database (through the GitKitClient java lib), but then these uploaded users can't log in The verifyPassword always returns "Incorrect password" ! The call to the uploadUsers looks like gitkitClient.uploadUsers('SHA1', new byte[0], gitkitUsers)
(We must provide an empty byte array as second param (hash key), since we get NPEs if we provide a null value)
The method that creates the GitkitUsers that are in the list is as follows:
private GitkitUser createGitkitUserFromUser(User user) {
GitkitUser gitkitUser = new GitkitUser()
gitkitUser.email = user.email
gitkitUser.localId = getLocalId(user)
gitkitUser.name = user.displayName
gitkitUser.hash = user.password?.bytes
if (user.pictureFileName) {
gitkitUser.photoUrl = user.getPictureUrl()
}
return gitkitUser
}
We see no way to further investigate. Did someone successfully use it ?
Make sure that the hashKey you use in setPassword() is the same one used in uploadUsers().
I am using the php SDK so I can't share code for you, but when I did NOT use the same hashKey for both places, I had the same problem.

Resources