I'm building a real-time poll in Firebase. Each vote is stored in a list field. To prevent having to pull every vote down to the client in order to count them, I cache the tallies for each choice in counter fields.
poll1
counts
choice1: 5
choice2: 2
choice3: 10
choice4: 252
voters
uid1 : choice1
uid6 : choice3
uid25: choice2
uid31: choice1
I'm currently updating the counter with the following transaction:
var fireCountPush = new Firebase(self.fireCountUrl+node+id);
fireCountPush.transaction(function(current_value) {
return current_value + 1;
}, function(error, committed, snapshot) {
if(committed) {
var fireVote = new Firebase(self.fireVotesUrl);
fireVote.child(self.user.uid).set(id);
}
});
However, I want to atomically add the user to the voters list, ideally in the same transaction. Unfortunately, the way I have it now, I'm having to add the user after the transaction commits successfully. This is a huge security issue since it can be easily disabled in the browser by editing the script.
Is there any way to both update the counter and add a user to the list of voters without having to download the entire object in the transaction?
The simplest answer here is to have a neutral party (a server script) monitor the list of voters and increment the counter. Then one only need make sure users add themselves by their uid and can only do so once.
I'm sure there are also some brilliant ways to do this entirely with security rules. I'm, unfortunately, not that brilliant, but here's a brute force approach you can improve on if you really want the pain of a client-only solution.
The plan:
The schema:
/audit/<$x>/<$user_id>
/voters/$user_id/<$x>
/total/<$x>
We prevent user from modifying audit/ if they have already voted (voters/$user_id exists), or if the audit record already exists (someone has already claimed that count), or if the vote is not incremented by exactly one:
"audit": {
"$x": {
".write": "newData.exists() && !data.exists()", // no delete, no overwrite
".validate": "!root.child('voters/'+auth.uid).exists() && $x === root.child('total')+1"
}
}
You would update audit
in a transaction, essentially trying to "claim" each increment until successful and cancelling the transaction (by returning undefined) any time the record to be added is not null (someone has already claimed it). This gives you your unique vote number.
To prevent any funny business, we store a list of voters, which forces each voter to only write into audit/ once. I can only write to voters if I've never voted before and only if an audit record has already been created with my unique vote number:
"voters": {
"$user_id": {
".write": "newData.exists() && !data.exists()", // no delete, no replace
".validate": "newData.isNumber() && root.child('audit/'+newData.val()).val() === $user_id"
}
}
Last, but not least, we update the counter to match our claimed vote id. It must match my vote number and it may only increase. This prevents a race condition where one user creates an audit record and voters record, but someone else has already increased the total before I finished my three steps.
"total": {
".write": "newData.exists()", // no delete
".validate": "newData.isNumber() && newData.val() === root.child('audit/'+auth.uid).val() && newData.val() > data.val()"
}
Updating the total, like adding the initial audit record, would be done in a transaction. If the current value of the total is greater than my assigned vote number, then I just cancel the transaction with undefined
because someone else has already voted after me and updated it higher. Not a problem.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With