Prevent double voting with AngularFire, Firebase - firebase

I'm running into trouble saving and retrieving values in Firebase using Angularfire v2 (http://angularfire.com/documentationv2.html , v2 is necessary because v1 seemed unable to order items by priority)
It's a simple vote-up-down app using Persona Login for authorization, the problem comes when the user votes on a 'story', I want to add his user id to 'story/users', but it seems the only way to do that without erasing all the other values is to use '$add'(push), which throws in a random key name which I have no idea how to query against.
So to prevent double voting I have to somehow ask if 'story/users' already has that user's id in there.
story---
users---
-J9LrtJwXnCI3ZYqsIz1: user-1
-J9Lriauhfdoaiuhfafd: user-2
Does anybody have any idea how to find out if user-1 is in 'story/users'?
Keep in mind Angularfire v2 has some major changes.
And if anybody has a better idea I'd be happy to hear it (but I'm really trying to avoid saving it as user-1/story-id, for other reasons)

Store the records by the user's ID, rather than a random push id. For example, you could create a votes path, and keep track of who has voted there:
/votes/user-1
/votes/user-2
Then you can write a security rule to prevent double voting as follows:
{
"rules": {
"votes": {
"$record_id": {
// something like this
".write": "auth.id === $record_id && data.val() === true"
}
}
}
}
You can perform the set in V2 angularFire by using the $set and $child commands.
dataRef.$child( user.id ).set( true ); // for example

Related

Data structure for Firebase one-to-one chat in flutter [duplicate]

In my main page I have a list of users and i'd like to choose and open a channel to chat with one of them.
I am thinking if use the id is the best way and control an access of a channel like USERID1-USERID2.
But of course, user 2 can open the same channel too, so I'd like to find something more easy to control.
Please, if you want to help me, give me an example in javascript using a firebase url/array.
Thank you!
A common way to handle such 1:1 chat rooms is to generate the room URL based on the user ids. As you already mention, a problem with this is that either user can initiate the chat and in both cases they should end up in the same room.
You can solve this by ordering the user ids lexicographically in the compound key. For example with user names, instead of ids:
var user1 = "Frank"; // UID of user 1
var user2 = "Eusthace"; // UID of user 2
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
user1 = "Eusthace";
user2 = "Frank";
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>
A common follow-up questions seems to be how to show a list of chat rooms for the current user. The above code does not address that. As is common in NoSQL databases, you need to augment your data model to allow this use-case. If you want to show a list of chat rooms for the current user, you should model your data to allow that. The easiest way to do this is to add a list of chat rooms for each user to the data model:
"userChatrooms" : {
"Frank" : {
"Eusthace_Frank": true
},
"Eusthace" : {
"Eusthace_Frank": true
}
}
If you're worried about the length of the keys, you can consider using a hash codes of the combined UIDs instead of the full UIDs.
This last JSON structure above then also helps to secure access to the room, as you can write your security rules to only allow users access for whom the room is listed under their userChatrooms node:
{
"rules": {
"chatrooms": {
"$chatroomid": {
".read": "
root.child('userChatrooms').child(auth.uid).child(chatroomid).exists()
"
}
}
}
}
In a typical database schema each Channel / ChatGroup has its own node with unique $key (created by Firebase). It shouldn't matter which user opened the channel first but once the node (& corresponding $key) is created, you can just use that as channel id.
Hashing / MD5 strategy of course is other way to do it but then you also have to store that "route" info as well as $key on the same node - which is duplication IMO (unless Im missing something).
We decided on hashing users uid's, which means you can look up any existing conversation,if you know the other persons uid.
Each conversation also stores a list of the uids for their security rules, so even if you can guess the hash, you are protected.
Hashing with js-sha256 module worked for me with directions of Frank van Puffelen and Eduard.
import SHA256 from 'crypto-js/sha256'
let agentId = 312
let userId = 567
let chatHash = SHA256('agent:' + agentId + '_user:' + userId)

Firebase Rules: Read restriction for dynamic child nodes

I'm trying to implement a Firebase rules read restriction in a data model that has a few nested dynamic child nodes.
I have the following data model:
/groupMessages/<groupId>/<messageId>/
{
"senderId": "<senderId>",
"recipientId": "<recipientId>",
"body": "..."
}
groupId, messageId, senderId and recipientId are dynamic ids. I would like to attach a listener to the /groudId node to listen to new messages. At the same time I only want users to read the message where the senderId or recipientId matches a corresponding auth.token value.
Due to Firebase cascading rules, if I allow the read at the groupId level without restrictions, I can't deny them on the message level.
{
"rules": {
"groupMessages"
"$groupId": {
".read": "auth != null"
}
}
}
}
I also haven't found a way to restrict the read rule on the groupId level to check for sender/recipientId of a message.
Any suggestions greatly appreciated.
As you've found, security rules cannot be used to filter data. But they can be used to restrict what queries can be performed on the data.
For example, you can query for all messages where the current user is the sender with:
var query = ref.child("groupMessages").child(groupId).orderByChild("senderId").equalTo(uid);
And you can secure access to the group's messages to only allow this query with:
{
"rules": {
"groupMessages": {
"$groupId": {
".read": "auth.uid != null &&
query.orderByChild == 'senderId' &&
query.equalTo == auth.uid"
}
}
}
}
The query and rules now exactly match, so the security rules will allow the query, while they'd reject a broader read operation. For more on this, see query based rules in the Firebase documentation
You'll note that this only works for a single field. Firebase Database queries can only filter on a single field. While there are workarounds by combining multiple values into a single property, I don't think those apply to your scenario, since they only work for AND queries, where you seem to want an OR.
You also seem to want to query on /groupMessages instead of on messages for a specific group. That also isn't possible: Firebase Database orders/filters on a property that is at a fixed path under each child of the node where you run the query. You cannot query across two dynamic levels, as you seem to be trying. For more on this see: Firebase Query Double Nested and Firebase query if child of child contains a value.
The common solution for your problem is to create a list of IDs for each user, which contains just the IDs of all messages (and/or the groups) they have access to.
userGroups: {
uid1: {
groupId1: true,
groupId2: true
},
uid2: {
groupId2: true,
groupId3: true
}
}
With this additional data structure (which you can much more easily secure), each user can simply read the groups they have access to, and your code then reads/queries the messages in each group. If necessary you can add a similar structure for the messages themselves too.
Finally: this type of recursive loading is not nearly as inefficient as many developers initially think, since Firebase pipelines the requests over an existing connection.

Firebase Chat - Get messages just for 2 users [duplicate]

In my main page I have a list of users and i'd like to choose and open a channel to chat with one of them.
I am thinking if use the id is the best way and control an access of a channel like USERID1-USERID2.
But of course, user 2 can open the same channel too, so I'd like to find something more easy to control.
Please, if you want to help me, give me an example in javascript using a firebase url/array.
Thank you!
A common way to handle such 1:1 chat rooms is to generate the room URL based on the user ids. As you already mention, a problem with this is that either user can initiate the chat and in both cases they should end up in the same room.
You can solve this by ordering the user ids lexicographically in the compound key. For example with user names, instead of ids:
var user1 = "Frank"; // UID of user 1
var user2 = "Eusthace"; // UID of user 2
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
user1 = "Eusthace";
user2 = "Frank";
var roomName = 'chat_'+(user1<user2 ? user1+'_'+user2 : user2+'_'+user1);
console.log(user1+', '+user2+' => '+ roomName);
<script src="https://getfirebug.com/firebug-lite-debug.js"></script>
A common follow-up questions seems to be how to show a list of chat rooms for the current user. The above code does not address that. As is common in NoSQL databases, you need to augment your data model to allow this use-case. If you want to show a list of chat rooms for the current user, you should model your data to allow that. The easiest way to do this is to add a list of chat rooms for each user to the data model:
"userChatrooms" : {
"Frank" : {
"Eusthace_Frank": true
},
"Eusthace" : {
"Eusthace_Frank": true
}
}
If you're worried about the length of the keys, you can consider using a hash codes of the combined UIDs instead of the full UIDs.
This last JSON structure above then also helps to secure access to the room, as you can write your security rules to only allow users access for whom the room is listed under their userChatrooms node:
{
"rules": {
"chatrooms": {
"$chatroomid": {
".read": "
root.child('userChatrooms').child(auth.uid).child(chatroomid).exists()
"
}
}
}
}
In a typical database schema each Channel / ChatGroup has its own node with unique $key (created by Firebase). It shouldn't matter which user opened the channel first but once the node (& corresponding $key) is created, you can just use that as channel id.
Hashing / MD5 strategy of course is other way to do it but then you also have to store that "route" info as well as $key on the same node - which is duplication IMO (unless Im missing something).
We decided on hashing users uid's, which means you can look up any existing conversation,if you know the other persons uid.
Each conversation also stores a list of the uids for their security rules, so even if you can guess the hash, you are protected.
Hashing with js-sha256 module worked for me with directions of Frank van Puffelen and Eduard.
import SHA256 from 'crypto-js/sha256'
let agentId = 312
let userId = 567
let chatHash = SHA256('agent:' + agentId + '_user:' + userId)

How to hide information in Firebase?

I have these requirements:
User will log in via email and multiple Oauth providers. I use function like User.findByEmail('1#1.com'). So I need to have permision to see the list of users before being authenticated.
User's email address, geolocation and age should be kept secretly from other users.
My first plan was:
users:{
$user-id:{
// public
".read" : true,
name,
created_at,
private: {
".read" : "auth.email === data.child('email').val()",
age,
email,
geolocation,
}
$piority = email
}
}
Then I realized no, it just doesn't work that way. So would anyone please tell me how to do it correctly? Thanks in advance.
P.S. Firebase really needs a filter/serialize method.
There are really several questions in this post. The first is how to store private data. One simple change you can make is to invert the public/private and user keys--they don't have to be nested in the same user record.
/users/$user_id/...public data...
/private/$user_id/...private data...
This makes securing the data quite a bit simpler.
Another question is regarding duplicate email detection. If we assume you are using simple login here, this is all pretty moot. You can check to see if an email address exists by simply trying to create the account. An error will be returned if the email is already registered.
If that doesn't solve it, you can still check this manually, without providing a list of users' emails. This is typically done with an index. When a new account is created, write something like this:
/email_index/$escaped_email/$userid ($userid is the value)
Now when you want to check if the email is available, you do a read like this:
var ref = new Firebase(URL);
function checkEmail(emailAddress, callback) {
ref.child('email_index/'+escapeEmail(emailAddress)).once('value', function(snap) {
callback(snap.val() !== null);
});
}
function escapeEmail(emailAddress) {
return (email || '').replace('.', ',');
}
checkEmail(EMAIL_ADDRESS, function(exists) {
console.log(EMAIL_ADDRESS + (exists? 'does' : 'does not') + ' exist!');
});
To prevent someone from listing the emails, you do something like this in your (amazingly flexible and really quite sophisticated, even for enterprise apps) security rules:
"email_index": {
// no .read rule here means that the data cannot be listed; I have to know the email address to check it
"$email_address": {
".read": true,
// it can only be claimed once and the value must be my user id
".write": "auth.uid === newData.val() && !data.exists()"
}
}

firebase security api read permission denied

I found a neat little example for permission based chat rooms using firebase security api here
Notice the "chat": {
// the list of chats may not be listed (no .read permissions here)
I actually need to list the chats a user belongs to when I load their inbox, however I can't seem to get the .read rule correctly.
Ive tried using the following rule which makes total sense but doesn't work:
"convos": {
".read" : "auth != null && data.child('users').hasChild(auth.id)",
I suspect the problem is that there is still a level between convo and users.. aka would make more sense to do:
"convos": {
".read" : "auth != null && data.child($key + '/users').hasChild(auth.id)",
$key : { ... }
But that's not allowed is complains about $key not existing yet.
How can I allow a user to pull all the convos they belongs to using this setup?
You can't use security rules to filter data. Generally, your data structure will be fairly dependent on your specific use case--most directly on how the data will be read back.
A general solution is to list the chats your user belongs to separate from the bulk chat data, i.e. to heavily denormalize, and access the chats individually.
/messages/$chat_id/... (messages chronologically ordered using push() ids)
/chats/$chat_id/... (meta data)
/my_chats/$user_id/$chat_id/true (the value here is probably not important)
Now to access all of my chats, I could do something like the following:
var fb = new Firebase(URL);
fb.child('my_chats/'+myUserId).on('child_added', function(snap) {
var chatID = snap.name());
loadChat(chatID);
});
function loadChat(chatID) {
fb.child('messages/'+chatID).on('child_added', function(snap) {
console.log('new message', chatID, snap.val());
});
}
You would still want security rules to validate the structure of chat messages, and access to a users' chat list, et al. But the functionality of filtering would be done with an index like this, or by another creative data structure.
I'm not completely sure how you're structuring your Firebase, but this might work:
"convos": {
$key : {
".read" : "auth != null && data.child('users').hasChild(auth.id)",
...
}

Resources