How to avoid store same values on DB - firebase

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.

Related

Unique field in Firestore database + Flutter

I'm trying to implement a normal authentication system in my app, but I'd like to create a new field for each user that is the "uniqueName" so users can search and add each other in their friends list. I was thinking of adding a textField in the signup form for the uniqueName and updating my User class adding a new String in this way:
class User {
String email;
String name;
String uniqueName;
String userID;
String profilePictureURL;
String appIdentifier;
...
}
Now, since I have this method for the email&password signup:
static firebaseSignUpWithEmailAndPassword(String emailAddress,String password,File? image,String name,) async {
try {
auth.UserCredential result = await auth.FirebaseAuth.instance
.createUserWithEmailAndPassword(
email: emailAddress, password: password);
String profilePicUrl = '';
if (image != null) {
await updateProgress('Uploading image, Please wait...');
profilePicUrl =
await uploadUserImageToFireStorage(image, result.user?.uid ?? '');
}
User user = User(
email: emailAddress,
name: name,
userID: result.user?.uid ?? '',
profilePictureURL: profilePicUrl);
String? errorMessage = await firebaseCreateNewUser(user);
if (errorMessage == null) {
return user;
} else {
return 'Couldn\'t sign up for firebase, Please try again.';
}
}
how do I have to modify it in order to add this new field in the registration? Since I have to check that the uniqueName insert by the user is effectively unique before creating a new user in the database, what can I do?
Furthermore, I think that it would be cool if this check is made concurrently to the filling of the form, how can I do it? (this is not necessary)
Thanks everyone for the answers
You have to save your users in a collection, then check if uniqueName already exists in the collection. If it exists, return error.
Then when a new user account is created, save the uniqueName.
// this function checks if uniqueName already exists
Future<bool> isDuplicateUniqueName(String uniqueName) async {
QuerySnapshot query = await FirebaseFirestore.instance
.collection('PATH_TO_USERS_COLLECTION')
.where('uniqueName', isEqualTo: uniqueName)
.get();
return query.docs.isNotEmpty;
}
// call the above function inside here.
static firebaseSignUpWithEmailAndPassword(String emailAddress, String password, File? image, String name,) async {
if (await isDuplicateUniqueName(name)) {
// UniqueName is duplicate
// return 'Unique name already exists';
}
// ... the rest of your code. Go ahead and create an account.
// remember to save the uniqueName to users collection.
I suggest doing the following steps:
Create your own users collection (for example users) in Firestore, which you might have done already. (I don't think that User is a good class name, since Firebase Authentication is using the same name. Try MyUser or something.)
Add authentication triggers that will ensure that whenever a Firebase user is added or deleted, it will also be added to or deleted from users collection, use Firebase uid as identifier.
Create a solution to check whether a uniqueName already exists in users collection. You can use a Firestore query, but in this case you have to allow unauthenticated access to read users, at least uniqueName field. (Since the user is not authenticated yet at this point.) A Firebase Cloud Function is another option.
When users enter their desired uniqueName, run the check before creating Firebase user. You can do it when user enters this or when you start the signup process.
If uniqueName is unique, you can try to create Firebase user. Be aware, this step can also fail (for example e-mail name taken etc.). Your users document will be created by the authentication trigger you set up in step 2.
Finally, you have to store this uniqueName in users collection. At this point you will have uid of the newly created Firebase user, so you can use Firestore set command with merge option set to true, so you don't overwrite other data.
It is important to note that you can't guarantee that the Firebase trigger already created the new document in users by the time you arrive to point 6, it is very likely that the trigger is still working or not even started yet. That's why you have to use set both in the authentication trigger and in your own code that sets uniqueName: which "arrives" first, will create the document, and the second will update it.
Also, for the same reason, you have to allow inserts and updates into users collection with Firestore rules. This might sound a little scary, but keep in mind that this is only your own user list to keep track of uniqueName, and authentication is based not on this, but on Firebase Authentication's user management which is well protected.
Last comment: this is not a 100% solution. It is quite unlikely, but theoretically can happen, that some else reserves a uniqueName between you check whether it's unique and the user is actually created. To mitigate this, it is a good idead to make the check just before Firebase user is created. Even in this case a slight chance remains for duplicates.

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/

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.

Angularfire generate new unique ID for non-email object

I'm making a web app using angularfire. I have a url for users at 'url.firebaseio.com/users'. I want to make another url to store chat-room type things at 'url.firebaseio.com/rooms'. When I create a new user using the $createUser() method, it stores the user's information under a unique ID created by $createUser() in the '/users' url. I want to do this with the new '/rooms' url, but I can't find a way to generate unique IDs in the same way without $createUser(). I can't use $createUser() because it requires an email address argument, and I just want to take in a name for the room and a password, all in an object under the unique ID for the room.
I can't think of any code to provide, so here's what a user looks like:
users: {
uniqueUserId: {
email: email#email.com,
name: name
}
}
And here's what I'd like a 'room' to look like:
rooms: {
uniqueRoomId: {
roomName: something
}
}
Is there a built-in way to do this? If not would it be best to generate IDs on my own?

Good way to replace invalid characters in firebase keys?

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");
}
}

Resources