The simplest answer: the neutral side (server script) controls the voter list and increases the counter. Then you just need to make sure that users add themselves through their uid and can do this only once.
I am sure there are also brilliant ways to do this completely with security rules. Unfortunately, Iβm not so brilliant, but here the brute-force approach can improve if you really want the pain of a client-only solution.
Plan:
- force the user to record the audit record first.
- make them add their name to the voter list second
- allow them to update the counter when both of these entries exist and match the voting number
Scheme:
/audit/<$x>/<$user_id> /voters/$user_id/<$x> /total/<$x>
We do not allow the user to change the audit / if they already voted (there is a voter / $ user_id), or if the audit record already exists (someone already claimed that the count), or if the vote does not increase by 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 should update audit in the transaction, essentially trying to βrequireβ each increment until you succeed and cancel the transaction (returning undefined) at any time when the added record is not null (someone has already stated this). This gives you a unique voting number.
To prevent a funny business, we keep a voter list that makes each voter record only once at a time. I can only write to voters if I have never voted before, and only if an audit record has already been created with my unique voting number:
"voters": { "$user_id": { ".write": "newData.exists() && !data.exists()", // no delete, no replace ".validate": "newData.isNumber() && root.child('audit/'+newData.val()).val() === $user_id" } }
Finally, but not least, we update the counter to match our declared id. It must match my number of votes, and it can only increase. This prevents the race condition when one user creates an audit record and a voter record, but someone else has already increased the total amount before I finish the 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 amount, such as adding an initial audit record, will be performed in the transaction. If the current value of the total amount is more than my assigned number of votes, I just canceled the transaction using undefined because someone else voted for me and updated it above. No problem.