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)
Related
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)
Use Case Summary
User A creates a story
User A shares story with unknown (to the app) User B via email (sent via cloud function)
User B receives an email about the story
User B visits app and creates a new account
User B sees/reads story create by User A
Note: stories can only be seen by whom they been shared with or created by
I'm building a role based access system. I've been looking at the role based access firestore documentation and I'm missing one piece.
Consider a story that can only be read by a user for which that story has been shared. Most examples including the firestore example use the UID has the key to identify shared users. However, that user may not currently be a user of the firebase app additionally how does a user assign that UID.
Story Data
{
title: "A Great Story",
roles: {
aliceUID: {
hasRole: true,
type: "owner",
},
bobUID: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.uid}.hasRole`, '==', true)
The second part could potentially be solved by maintaining a separate user collection then you could find the user from their email address, but this doesn't address users that have never logged in.
The user who intends to share a story could add the user with an email address. Then using firebase functions we could send an email to notify the user of the shared story and the user could login to the app and read that story.
If we proceed with this method then you would not have a UID but only an email address as the key.
Story Data
{
title: "A Great Story",
roles: {
alice#yahoo.com: {
hasRole: true,
type: "owner",
},
bob#gmail.com: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.email}.hasRole`, '==', true)
Updated Firestore Rule - from documentation
function getRole(rsc) {
// Read from the "roles" map in the story document.
return rsc.data.roles[request.auth.uid] || rsc.data.roles[request.auth.token.email];
}
I can not get the email query to work. This SO issue mentions that
Unfortunately dots are not allowed as a map key. So email addresses won't work.
I don't see why this would be a conflict on the rules side. It does make for a likely invalid where clause
e.g.
.where(`roles.${user.email}.hasRole`, '==', true) -> .where(`roles.bob#gmail.com.hasRole`, '==', true)
That looks like invalid JS and unfortunately [ and ] are invalid characters so we can't do
.where(`roles[${user.email}]hasRole`, '==', true)
The final thing I've seen is using for this Firebase talk is to escape the email address using something like
function encodeAsFirebaseKey(string) {
return string.replace(/\%/g, '%25')
.replace(/\./g, '%2E')
.replace(/\#/g, '%23')
.replace(/\$/g, '%24')
.replace(/\//g, '%2F')
.replace(/\[/g, '%5B')
.replace(/\]/g, '%5D');
};
This appears to fix the query where clause and it's a valid data structure, but it's not a valid Firestore rule meaning it has no true security enforcement.
Any ideas on how to implement this? Please include valid data structure, firestore rules, and query. I've shown and seen many examples that get two out of the three which are all non-working solutions.
Thanks!
The basic issue was that I did not know how to properly formulate a valid query. It turns out that you don't need to create a query in one line.
You can use FieldPath to construct your query parameter.
var path = new firebase.firestore.FieldPath('roles', email ,'hasRole');
firebase.firestore().collection('stories').where(path, '==', true)
This solves for the original missing piece.
This is a use case for Control Access with Custom Claims and Security Rules.
The Firebase Admin SDK supports defining custom attributes on user
accounts. This provides the ability to implement various access
control strategies, including role-based access control, in Firebase
apps. These custom attributes can give users different levels of
access (roles), which are enforced in an application's security rules.
User roles can be defined for the following common cases:
Giving a user administrative privileges to access data and resources.
Defining different groups that a user belongs to.
Providing multi-level access:
Differentiating paid/unpaid subscribers.
Differentiating moderators from regular users.
Teacher/student application, etc.
You'll need to stand up a node server (skill level low). A script like below works to generate the claims.
var admin = require('firebase-admin');
var serviceAccount = require("./blah-blah-blah.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://my-app.firebaseio.com"
});
admin.auth().setCustomUserClaims("9mB3asdfrw34ersdgtCk1", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
Then on your client side, do something like:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
//is email address up to date? //do we really want to modify it or mess w it?
switch (user.providerData[0].providerId) {
case 'facebook':
case 'github':
case 'google':
case 'twitter':
break;
case 'password':
// if (!verifiedUser) {
// }
break;
}
//if admin
firebase.auth().currentUser.getIdToken().then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin or whatever
if (!!payload['admin']) {
switch (thisPage) {
case "/admin":
showAdminStuff();
break;
}
}
else {
if(isAdminPage()){
document.location.href="/";
}
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});
From what I have gathered, you want to make a story private but shareable with anyone. Your biggest concern is for users who do not have the app but have the share link.
And therefore your biggest problem is that the way firebase works means that you cant limit access to your data without using some sort of login.
If you are ok with requiring new users to login, then your answer should just be Dynamic Links. These links are persistent all the way though installation and login which means that anyone can be given a dynamic link that has story access data attached. You would merely need to add a listener to your app's mainActivity or AppDelegate equivalent to record the dynamic link data and run a specif task after login.
If you wish to stay away from the login completely, then you set up the dynamic link to bypass the login process and direct the new-install-user directly to the story. This second option however, requires a bit more work and is less secure because you will probably be forced to duplicate the story data for open access to anyone with the proper link to the story.
I'm trying to setup a friend system in Firestore. My data model looks like this at the moment:
collection("users") ->
document("user1")
document("user2")
...
A document in the users collection contains data like the name, email... of the user. I'd like to enable a user to have friends now, but I'm unsure about the best way to model this.
So, I'd for sure add a friends field in the documents of the users, but what should this field contain? My first thought was a pointer to a new collection called friends in which the documents are users. Something like this:
collection("users") {
document("user1") {
name:user1,
friends: -> collection("friends") {
document("user2"),
...
}
}
}
This seems reasonable, but that'd mean that I'd have a lot of duplicate data in my database because each user that has friends will be duplicated in a friends collection. Should I worry about this or is this normal in a Firestore database structure?
Would it perhaps be possible to point to a document in the users collection from the friends collection? Something like:
collection("users") {
document("user1") {
name:user1,
friends: -> collection("friends") {
document, -----
... |
} |
}, |
document("user2")<-
}
Or should I throw away the thought of using a collection for friends and just keep a list with uids of all friends of the user?
Seems you are using two separate collections for users and friends first all you can do it by one collection. But I don't want to go there may be there was another scenario.
As your separate collection way, you can design your friends collection model to meet no duplication:
{
name : 'Name',
email : 'email#mail.com'
has_connected : {
'user1' : true // here you can use anyother unique key from user
}
}
The thing is that firestore recommend this types of design for query and for faster performance you can make that has_connected key as index.
In this approach, you have to check during adding new friend by email or any other unique key. if exists then just put another key into has_connected with the respective user. e.g user2 : true.
Finally, for fetching all friends for a user you have to do a query like this: e.g: in javascript
let ref = firebase.firestore().collection("friends");
ref
.where(`has_connected.${username}`, "==", true)
.get()
.then(//do your logic)
.catch()
Thanks
I would like to make a one to one chat. Each user can contact another user.
Json structure would be :
{
"messages" :
"user1UID_user2UID" : {
auto generated ID : {
"text" : "hello",
"timestamp" : 192564646546,
"name" : "user1"
},
auto generated ID : {
"text" : "hi",
"timestamp" : 192564646554,
"name" : "user2"
}
}
}
When user1 connects to the app, he can see the list of every conversation of which he is a part.
Let's say he had initiated a conversation with user 2, and user 3 has a conversation with him too.
So we would have the following children :
user1UID_user2UID
user3UID_user1UID
How can I retrieve all the conversations User1 is involved in to ?
constructor(db: AngularFireDatabase) {
this.messages= db.list('/messages/' + user1UID + "_" + user2UID); //but I don't know user2UID at this moment
}
Can I make a Regex or do I have to store the conversation key (somewhere) every time it concerns him ?
Or I'm completely wrong and I do not look at the problem the right way?
The key naming schema you use for the chat rooms is a variant of my answer here: http://stackoverflow.com/questions/33540479/best-way-to-manage-chat-channels-in-firebase. It's a variant, since you don't seem to order the UIDs lexicographically, which I recommend.
All my proposed algorithm does is generate a reproducible, unique, idempotent key for a chat room between specific users. And while those are very important properties for a data model, they don't magically solve other use-cases.
As often the case in NoSQL data modeling, you'll have to model the data to fit with the use-cases you want. So if your app requires that you show a list of chat rooms for each user, then you should include in your data model a list of chat rooms for each user:
userChatRooms
user1UID
user1UID_user2UID
user1UID_user3UID
user2UID
user1UID_user2UID
user1UID_user3UID
user3UID
user1UID_user3UID
Now getting a list of the chat rooms for a user is as easy as reading /userChatRooms/$uid.
Lets assume I'm trying to build a group messaging application, so I designed my database structure to look like so:
users: {
uid1: { //A user id using push()
username: "user1"
email: "aaa#bbb.ccc"
timestampJoined: 18594659346
groups: {
gid1: true,
gid3: true
}
}
uid2: {
username: "user2"
email: "ddd#eee.fff"
timestampJoined: 34598263402
groups: {
gid1: true,
gid5: true
}
}
....
}
groups: {
gid1: { //A group id using push()
name: "group1"
users: {
uid1: true,
uid2: true
}
}
gid2: {
name: "group2"
users: {
uid5: true,
uid7: true,
uid80: true
}
}
...
}
messages: {
gid1: {
mid1: { //A message id using push()
sender: uid1
message: "hello"
timestamp: 12839617675
}
mid2: {
sender: uid2
message: "welcome"
timestamp: 39653027465
}
...
}
...
}
According to Firebase's docs this would scale great.
Now lets assume that inside my application, I want to display the sender's username on every message.
Querying the username for every single message is obviously bad, so one of the solutions that I found was to duplicate the username in every message.
The messages node will now look like so:
messages: {
gid1: {
mid1: { //A message id using push()
sender: uid1
username: "user1"
message: "hello"
timestamp: 12839617675
}
mid2: {
sender: uid2
username: "user2"
message: "welcome"
timestamp: 39653027465
}
...
}
...
}
Now I want to add the option for the user to change his username.
So if a user decides to change his username, it has to be updated in the users node, and in every single message that he ever sent.
If I would have gone with the "listener for every message" approach, then changing the username would have been easy, because I would have needed to change the name in a single location.
Now, I have to also update the name in every message of every group that he sent.
I assume that querying the entire messages node for the user id is a bad design, so I thought about creating another node that stores the locations of all the messages that a user has sent.
It will look something like this:
userMessages: {
uid1: {
gid1: {
mid1: true
}
gid3: {
mid6: true,
mid12: true
}
...
}
uid2: {
gid1: {
mid2: true
}
gid5: {
mid13: true,
mid25: true
}
...
}
...
}
So now I could quickly fetch the locations of all the messages for a specific user, and update the username with a single updateChildren() call.
Is this really the best approach? Do I really have to duplicate so much data (millions of messages) only because I'm referencing a dynamic value (the username)?
Or is there a better approach when dealing with dynamic data?
This is a perfect example of why, in general, parent node names (keys) should be disassociated from the values they contain or represent.
So some big picture thinking may help and considering the user experience may provide the answer.
Now lets assume that inside my application, I want to display the
sender's username on every message.
But do you really want to do that? Does your user really want to scroll through a list of 10,000 messages? Probably not. Most likely, the app is going to display a subset of those messages and even at that probably 10 or 12 at a time.
Here's some thoughts:
Assume a users table:
users
uid_0
name: Charles
uid_1
name: Larry
uid_2:
name: Debbie
and a messages table
messages
msg_1
sender: uid_1
message: "hello"
timestamp: 12839617675
observers:
uid_0: true
uid_1: true
uid_2: true
Each user logs in and the app performs a query that observes the messages node they are part of - the app displays displays the message text of the message as well as each users name that's also observing that message (the 'group').
This could also be used to just display the user name of the user that posted it.
Solution 1: When the app starts, load in all of the users in the users node store them in dictionary with the uid_ as the key.
When the messages node is being observed, each message is loaded and you will have the uid's of the other users (or the poster) stored in the users_dict by key so just pick their name:
let name = users_dict["uid_2"]
Solution 2:
Suppose you have a LOT of data stored in your users node (which is typical) and a thousand users. There's no point in loading all of that data when all you are interested in is their name so your could either
a) Use solution #1 and just ignore all of the other data other than the uid and name or
b) Create a separate 'names' node in firebase which only keeps the user name so you don't need to store it in the users node.
names:
uid_0: Charles
uid_1: Larry
uid_2: Debbie
As you can see, even with a couple thousand users, that's a tiny bit of data to load in. And... the cool thing here is that if you add a listener to the names node, if a users changes their name the app will be notified and can update your UI accordingly.
Solution 3:
Load your names on an as needed basis. While technically you can do this, I don't recommend it:
Observe all of the messages nodes the user is part of. Those nodes will be read in and as they are read in, build a dictionary of uid's that you will need the names of. Then perform a query for each user name based on the uid. This can work but you have to take the asynchronous nature of Firebase into account and allow time for the names to be loaded in. Likewise, you could load in a message, then load in the user name for that message with the path: users/uid_x/user_name. Again though this get into an async timing issue where you are nesting async calls within async calls or a loop and that should probably be avoided.
The important point with any solution the user experience and keeping your Firebase structure as flat as possible.
For example, if you do in fact want to load 10,000 messages, consider breaking the message text or subject out into another node, and only load those nodes for your initial UI list. As the user drills down into the message, then load the rest of the data.
Steps to follow:
fetch username at every restart of app
cache them locally
show username from cache based on uid
done
Note: how you fetch username depends on your way of implementation
You only need this structure
mid1: { //A message id using push()
sender: uid1
message: "hello"
timestamp: 12839617675
}
The username can be read from the users directly "users/uid1/username" using a single value event listener after you read each child. Firebase is supposed to be used with sequential calls, since you cannot create complex queries like in SQL,
And just to keep it efficient you could:
1)Create a reference dictionary to use it as a cache handler in which after you read every message you verify if you have the value for each key:
[uid1:"John",uid2:"Peter",....etc...]
And if the key doesn't exist you add with the single value listener pointing to /users/$uid/username that handles the "add to cache" in its callback
2)Use the limitTo startAt and endAt queries to paginate the listener and avoid bringing data the user won't see
*There is no need to actually keep updating all the messages and all the nodes with every user change, imagine a chat group with 100 users in which every user have 20 messages ...2000 updates with your single updateChildren() call that would be extremely inefficient, since it is not scalable and you are updating data that surely no user will ever see again in a real life scenario (like the first message of the 2000 chat messages)