Consider the following code for atomic writes across multiple locations in FireBase:
var ref = new Firebase("https://<YOUR-FIREBASE-APP>.firebaseio.com");
var newPostRef = ref.child("posts").push();
var newPostKey = newPostRef.key();
var updatedUserData = {};
updatedUserData["users/"+authData.uid+"/posts/" + newPostKey] = true;
updatedUserData["posts/" + newPostKey] = {
title: "New Post",
content: "Here is my new post!"
};
ref.update(updatedUserData, function(error) {
if (error) {
console.log("Error updating data:", error);
}
});
This approach could be used to update the post at different locations, but how to enforce the atomic update at the server side? (via rules).
How can I make sure that the users cannot update the location /posts/ (via its direct reference) without populating the users/UID/posts/ or vice versa?
There are many possible such "business rules", so I'll pick one and implement that. Let's say that any post that a user refers to must exist. So you can only write to /users/myuid/posts/mypostid if /posts/mypostid exists. I'll also implement basic validation of the posts themselves.
{
"posts": {
"$postid": {
".validate": "hasChildren(['title', 'content'])",
"title": {
".validate": "newData.isString()"
},
"content": {
".validate": "newData.isString()"
},
"$other": {
".validate": false
}
}
},
"users": {
"$uid": {
"posts": {
"$postid": {
".validate": "newData.parent().parent().parent().parent().child('posts').child($postid).exists()
}
}
}
}
}
The biggest trick here is the newData.parent().parent()... bit, which ensures that we get the posts in the new data.
You have a habit of asking things like "how can I ensure that method ABC was used to update the data?", which is rarely the right way to think about matters. In the rules above I focus on validating the structure of the data and really don't care what API calls might lead to that data.
Related
I'm working on a chat module using fiebase , I have a structure as below .
My main node is mychatexperiment
inside it I have another node ChatListing and some other chat keys.
Today I set up setValues in my structure and when I passed my url without any node it deletes all of my data inside the parent node.
What i want is to set up the rules
One can create the node in any case
One can update the node in any case
One can not delete the node in any case
One Can only update or set the data inside ChatListing
I was trying using this but it does not work . any idea how to implement these things .
{
"rules": {
".write": "!data.exists() || !newData.exists()",
"read" : true
}
}
Note : I'm not using any authentication so need to implement rules without any authentication
Revise Requirements :
I have a structure where I have a single parent node and inside it I have multiple chat keys that is generated through firebase , Inside chat keys I have a node of individual message inside that chat .
The second thing which is most important is that I have a node called chatlisting in which I am storing my userids in a unique format like If My userid is 5 so inside chatlisting I am storing 5 and inside that 5 I have all the chat keys nodes which are related to me .
{
"ChatListing": {
"1126_2": { //userUnique key
"MjmhpHb6qR7VSkYzbjI": { // 1126 userid has this chat key and inside this chat last msg was welcome and its unread count is 0
"UnReadCount": 0,
"lastmessage": "Welcome",
"lastmessagetime": 1631870264251
}
},
"4184_1": {
"MjmhpHb6qR7VSkYzbjI": { // 4184 userid has this chat key as well and inside this chat last msg was welcome and its unread count is 1
"UnReadCount": 1,
"lastmessage": "Welcome",
"lastmessagetime": 1.6318646965369204E12
}
}
},
"MjmhpHb6qR7VSkYzbjI": { // chat key
"-MjmhpQbBaL7EbHPHayA": { // mesg key
"data": "Thankyou",
"time": 1.6318646965369204E12,
"type": 0,
"user": 4184 // the msg is sent by this user
},
"-Mjn21A4067dT4emYe05": { // another msg in the same chat
"data": "Welcome",
"time": 1631870264251,
"type": 0,
"user": 1126 // the msg is sent by this user
}
}
}
What I want is to setup the rules in which no one can run update , set or delete inside parent node (except ChatList node) . Any one can create chat keys and read chat keys inside parent node , nothing else they can do .
but inside chatlist they can perform create read , set and update(not delete) as I need to update the last message in this node against user chat .
So reusing the points as covered by my other answer, you would apply those rules using:
{
"rules": {
"ChatListing": {
"$userid": { // the user's unique ID
// anyone who knows this user ID can read their messages
".read": true,
"$chatid": { // a chatroom the user is in
// data stored here MUST have this shape (with nothing else)
// {
// UnReadCount: number,
// lastmessage: string,
// lastmessagetime: number
// }
// Data may be created or updated, but not deleted
".validate": "newData.child('UnReadCount').isNumber() && newData.child('lastmessage').isString() && newData.child('lastmessagetime').isNumber()",
"UnReadCount": { ".write": "newData.exists()" },
"lastmessage": { ".write": "newData.exists()" },
"lastmessagetime": { ".write": "newData.exists()" }
}
}
},
// when using $ keys at the same level as defined keys,
// this rule will catch everything that doesn't match
// the above rules
"$chatId": { // a chatroom where messages can be sent
// anyone who knows this chat ID can read its messages
".read": true,
"$msgId": { // a message in this chatroom
// Data stored here MUST have this shape (with nothing else)
// {
// data: string,
// time: number
// type: number,
// user: string, // see note
// }
// Data may be created, but not edited or deleted
// change like above if needed
".validate": "newData.child('data').isString() && newData.child('time').isNumber() && newData.child('type').isNumber() && newData.child('user').isString()",
"data": { ".write": "!data.exists()" },
"time": { ".write": "!data.exists()" },
"type": { ".write": "!data.exists()" },
"user": { ".write": "!data.exists()" }
}
}
}
}
Notes:
Don't use numeric user IDs as they are easily guessable, generate something random. You could even use const userId = push(getReference(getDatabase())).key. Consider securing the data with anonymous authentication.
Unlike your requirements, I have made the messages in the chat immutable. Once sent, no one can edit them. This prevents someone other than the sender from coming in and changing the message. With authentication, edits could be allowed because it's more secure.
Take note how unlike my /cars example, I haven't put ".read": true at the root of the database or at /ChatListing. This prevents someone coming along and running either of the below pieces of code to pull all of your stored data or pull all stored user IDs at once which will then allow them to find messages not meant for them. It does not prevent brute-forcing the data though.
const rootRef = ref(getDatabase());
rootRef
.then((snapshot) => {
console.log(snapshot.val()) // all data in database!
});
const chatListingRef = ref(getDatabase(), "ChatListing");
chatListingRef
.then((snapshot) => {
const usersArray = [];
const chatIdSet = new Set();
snapshot.forEach(userData => {
usersArray.push(userData.key)
userData.forEach(lastChatData => chatIdSet.add(lastChatData.key));
});
// logs all user IDs in the database!
console.log("User IDs:", usersArray)
// logs all chatroom IDs in the database!
console.log("Chatroom IDs:", [...chatIdSet])
});
Let's take a look at "Instagram-like" app, as an example.
In the feed we got posts, with user avatar and name at the top, photo or video below, and last comments, likes count and post time at the bottom.
Basically, at the client I'm waiting to get from backend something like
{
username: "John",
avatar:"some_link",
photo:"photo_url",
likes:"9",
time:"182937428",
comments:[comments there]
}
but using Firebase, I need to store data in more flat way. so there will be "users", "posts" and "comments" in data JSON.
How am I suppose to aggregate data from those nodes in some kind of single object, which is easy to use at client?
Or should I ask Firebase for posts, than for all users in it, and for all their comments, and do aggregation after all three 'requests' are done?
You should implement "shallow" tree structure, and use references where needed.
That means that for most cases in your app you should use the object as at is, Making sure that it contain the "essential data" (in the example below "the chat title"), and keys for "further" information (in the example, keys to the "members").
from firebase docs (https://firebase.google.com/docs/database/web/structure-data):
bad
{
// This is a poorly nested data architecture, because iterating the children
// of the "chats" node to get a list of conversation titles requires
// potentially downloading hundreds of megabytes of messages
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"messages": {
"m1": { "sender": "ghopper", "message": "Relay malfunction found. Cause: moth." },
"m2": { ... },
// a very long list of messages
}
},
"two": { ... }
}
}
good
{
// Chats contains only meta info about each conversation
// stored under the chats's unique ID
"chats": {
"one": {
"title": "Historical Tech Pioneers",
"lastMessage": "ghopper: Relay malfunction found. Cause: moth.",
"timestamp": 1459361875666
},
"two": { ... },
"three": { ... }
},
// Conversation members are easily accessible
// and stored by chat conversation ID
"members": {
// we'll talk about indices like this below
"one": {
"ghopper": true,
"alovelace": true,
"eclarke": true
},
"two": { ... },
"three": { ... }
},
// Messages are separate from data we may want to iterate quickly
// but still easily paginated and queried, and organized by chat
// conversation ID
"messages": {
"one": {
"m1": {
"name": "eclarke",
"message": "The relay seems to be malfunctioning.",
"timestamp": 1459361875337
},
"m2": { ... },
"m3": { ... }
},
"two": { ... },
"three": { ... }
}
}
https://github.com/kristinyim/ClassroomChat
I want to add an upvoting feature to the messages on this chatroom similar to what you have on GroupMe, but I'm new to React and built this off of a tutorial so don't know where to even begin. I'm good with webdev but am just getting started with the basics of React.js and Firebase. Thanks!
NB: There are many ways to achieve this, so the following is just a suggestion.
First you must think of how you want to store your data in the database. If you have users, messages and message-likes, you could structure it like this:
"root": {
"users": {
"$userId": {
...
"messages": {
"$messageId1": true,
"$messageId2": true,
...
}
}
},
"messages": {
"$messageId": {
"author": $userId,
"timestamp": ServerValue.TIMESTAMP
}
},
"likesToMessages": {
"$messageId": {
"$likeId": {
liker: $userId,
"message": $messageId,
"timestamp": ServerValue.TIMESTAMP
}
}
}
}
Whenever a user clicks "like" on a message, you want to write to
var messageId = ?; // The id of the message that was liked
var like = {
liker: currentUserId, // id of logged in user
message: messageId,
timestamp: firebase.database.ServerValue.TIMESTAMP
};
firebase.database.ref().child('likesToMessages').child(messageId).push(like);
Then you get a new like in the database, matching the proposed structure.
Then, when you want to read and show the count of likes for a message, you can do like this:
const Message = React.createClass({
propTypes: {
message: React.PropTypes.object,
messageId: React.PropTypes.string // you need to add this prop
}
componentWillMount() {
firebase.database.ref().child('likesToMessages').child(this.props.messageId).on('value', this.onLikesUpdated)
}
onLikesUpdated(dataSnapshot) {
var likes = snap.val();
this.setState({
likes
});
}
render() {
const {name, message} = this.props.message;
const emojifiedString = emoji.emojify(message);
return (
<p>
{name}: {emojifiedString} [{this.state.likes.length}♥]
</p>
);
}
});
Also, in your database security rules, you'd want to index by timestamp for message and like so you can quickly query the newest messages.
Also, feel free to check out a similar app I made, code in GitHub and demo on wooperate.firebaseapp.com.
I'm having a hard time figuring out how to validate a multi-location update where the updates depend on each other.
Consider the following structure:
"votes": {
"$post_id": {
"$answer_id": {
"$user_id": {
".write": "...",
".validate": "..."
}
}
}
}
"users": {
"$user_id": {
"votes": {
"up": {
".write": "...",
".validate": "..."
},
"down": {
".write": "...",
".validate": "..."
}
}
}
}
The users can vote on posts' answers with -1 / +1 (or remove their votes, so with null as well). So far so good, I can validate that no problem. My problem comes when I want to validate the user's up/down vote counter as well.
Example scenario: user A votes on an answer with +1, which would also increment user B's up counter with 1. How can I validate the up field so that it only gets incremented (or decremented) when there's an actual new vote for that.
Also there are scenarios like when a user has already voted +1 and then changes it directly to -1. I'm having a really hard time validating updates like this.
Should I just consider adding a server layer and do every single updates through the server? Or is my approach totally wrong here (or the data structure?). Adding a server layer would pretty much solve every validation issue, but also would add one more point of failure so I'm trying to avoid that.
Edit:
Update function
function vote(postID: string, answerID: string, author: string, oldVal: number, newVal: number): firebase.Promise<void> {
let voteValue: number = newVal == 0 ? null : newVal; // -1, 0, 1, could be changed to boolean
return this.ref.child(`users/${author}/votes`).once('value', count => {
let updates = {};
updates[`votes/${postID}/${answerID}/${this.authService.current.$key}`] = voteValue;
if (voteValue == 1) {
updates[`users/${author}/votes/up`] = ++count.val().up;
if (oldVal == -1) {
updates[`users/${author}/votes/down`] = --count.val().down;
}
}
if (voteValue == -1) {
updates[`users/${author}/votes/down`] = ++count.val().down;
if (oldVal == 1) {
updates[`users/${author}/votes/up`] = --count.val().up;
}
}
if (voteValue == null && oldVal == -1) {
updates[`users/${author}/votes/down`] = --count.val().down;
}
if (voteValue == null && oldVal == 1) {
updates[`users/${author}/votes/up`] = --count.val().up;
}
this.ref.update(updates);
});
}
When an answer's author's current votes are 0/0 and another user upvotes one of his answers it would create an update like:
"votes/-KM0CMCIQuBsGWQAjhRQ/-KM0CVmhK_7JQcxtdixl/fxpxj1Ky4yVpEeeB5PZviMjqNZv1": 1
"users/spAnCEKTTjX1GgdybQZIlXRI9IG2/votes/up": 1
I wrote following simple presence code in JavaScript (based upon https://firebase.google.com/docs/database/web/offline-capabilities#section-sample):
var app = firebase.initializeApp(config);
var mainRef = app.database().ref();
var session = null;
var connected = false;
function do_sessionSubscribe(subscription) {
if (!subscription.entry) {
subscription.entry = subscription.parent.push(true);
subscription.entry.onDisconnect().remove();
}
}
function do_sessionUnsubscribe(subscription) {
if (subscription.entry) {
subscription.entry.remove();
subscription.entry = null;
}
}
mainRef.child(".info/connected").on("value", function(snap) {
connected = snap.val() === true;
if (session) {
if (connected) {
do_sessionSubscribe(session.subscription);
} else {
// workaround
//do_sessionUnsubscribe(session.subscription);
}
}
});
function closeSession() {
if (session) {
do_sessionUnsubscribe(session.subscription);
session = null;
}
}
function openSession(uid) {
session = { uid: uid, subscription: { parent: mainRef.child("session/user/" + uid), entry: null } };
if (connected) {
do_sessionSubscribe(session.subscription);
}
}
app.auth().onAuthStateChanged(function(user) {
closeSession();
if (user && user.uid) {
openSession(user.uid);
}
});
Security rules:
"session": {
"user": {
"$uid": {
".read": "auth.uid === $uid",
".write": "auth.uid === $uid",
"$session": {
".validate": "newData.val() === true"
}
}
},
}
The idea is that each active connection of a user will create /session/user/$uid/$session upon connecting/signing in and delete it when disconnecting/signing out.
Therefore in order to obtain a list of online users it should be sufficient to get /session/user with shallow=true.
The problem is that sometimes a session isn't cleaned up and stays under /session/user/$uid forever. This is then interpreted like if a user was online all the time.
I discovered that in order to easily reproduce the issue it is sufficient to block access to securetoken.googleapis.com (I use Google authentication), wait an hour and close the browser.
I tried to workaround the problem by calling remove() on disconnection. This cleans up the stale session as soon as the client gets reconnected (this is too late, but better late than never...). However, when user closes it's browser after loosing internet connection and then the auth token expires before sockets time out, the stale session persists forever.
What value of auth.uid is used during checking security rules when auth token used for registering onDisconnect() action is already expired?
How to make this presence system fully reliable without compromising security?