Best way to manage Chat channels in Firebase - javascript

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

Efficient way making "Like" function without duplication?

I am making a "Like" function (facebook or instagram kind of), but not sure what is the right way to do it.
I could think of 2 ways.... (User cannot like same article twice)
A. "User" data has an array of "Article" IDs...
// simplified user schema MongoDB
const UserSchema ={
id:ObjectID,
username:String,
likes:[{type:ObjectID,ref:"Article"}]
}
// simplified article schema
const AriticleSchema = {
id:ObjectID,
title:String,
content:String,
likes:Number,
}
B. "Article" data has an array of "User" IDs...
// simplified user schema MongoDB
const UserSchema ={
id:ObjectID,
username:String,
}
// simplified article schema
const AriticleSchema = {
id:ObjectID,
title:String,
content:String,
likes:[{type:ObjectID,ref:"User"}],
}
I tried both ways and they all worked fine when I only have few users and few articles.
but What if I have thousands of "User"s and thousands of "Article"s? I am worrying that everytime I request "User" data or "Article" data(let's say several at a time), I also have to bring arrays of thousands? I think there must be better way to do this...
Do you know how people or companies do this? I want to know the concept of how "Like" function works.
Thank you.
** Adding some details **
I want "User" can login for reading articles, and press "like button" to like it. Article "like" will go up by 1 every time unique user likes it (no duplicate). Somebody who already liked the " article" they can "unlike" it or "user" will see they already liked the "article", which means we gotta know that "user" like this "article" or not. Other people dosen't need to know.
I'll extrapolate on a solution by going through the requirements you elaborated step by step.
User can login
This will require some system for authorization. You could perhaps use another Mongo table dedicated to this sort of thing - at the minimum it should link some authorization token to a user id.
const AuthSchema = {
user_id: ObjectID,
auth_token: string,
}
The way you get this auth token is through various means - really depends on how you auth your users, e.g. phone auth or username/password. All that's a bit beyond the scope of this answer.
Article "like" will go up by 1 every time unique user likes it (no duplicate). Somebody who already liked the " article" can "unlike" it or "user" will see they already liked the "article"
A Redis Set would model this quite well. Every time a user likes an article, add their user_id into a Redis Set for that article. The key for such set could look like this:
article:${article_id}:likes
Get the number of people who liked the article by taking the size of its set (full of unique user_ids). Whenever someone unlikes an article, remove their user_id from the set.
Further reading:
Redis Sets and other Redis datatypes
Redis sadd command.

Modifying array property of PFUser - Parse

I have an array property called courses on my User table in Parse. Any idea why I might getting Cannot modify user XTC9aiDZlL. code=206, message=Cannot modify user XTC9aiDZlL. when I do the following:
user.remove('courses', deletedCourse);
user.save()
where deleteCourse is the course PFObject to delete
Are you signed in as the user you're trying to modify? That can cause problems like this, as Parse usually just lets a user modify themself & the objects they've created.
If you're signed in as the same user you're trying to edit that's another story. This is a glitch that seems to be popping up in the Parse server recently. It's not the best solution but for now you'll need to modify the ACL when you create the user, like this:
let user = PFUser()
let acl = PFACL()
acl.getPublicWriteAccess = true
acl.getPublicReadAccess = true
user.acl = acl

MongoDB + Node JS + Role Based Access Control (RBAC)

Am currently learning MEAN stack, developing a simple TODO's app and want to implement Role Based Access Control (RBAC) for that. How do i set up roles & permission on MongoDB.
I want 3 roles (roles may look funny but this is purely to learn) :
GOD
SUPER HERO
MAN
GOD - similar to super admin, can do anything in the application. C,R,U,D permissions for TODO's and for other users too. Can Create a TODO & assign it to any SUPER HERO or MAN directly. Update or Delete either a TODO or a User at any point in time.
SUPER HERO - similar to admin, has super power to do anything on his personal Data - C,R,U,D for TODO's. Can't create any users. Can only Read & add comments for TODO's created by GOD & assigned to him/her.
MAN - Can only Read and add comments to TODO's assigned to him/her.
To sum it up :
GOD - C,R,U,D [Global Level]
SUPER HERO - C,R,U,D [Private] + R,U [Assigned to him]
MAN - R,U [Assigned to him]
I understand that i need to have USERS & ROLES collections. Where ROLES inturn should have PERMISSIONS etc. How do i wire them all ?
I like names given to roles - GOD, SUPER HERO & MAN, easy to understand.
As you are using MEAN stack and much of routes validation happens on node, i would prefer keeping roles table simple.
Roles :
{
_id : 1,
name : GOD,
golbalPerms : true
},
{
_id : 2,
name : SUPER HERO,
privatePerms : true
},
{
_id : 3,
name : MAN
}
Users :
{
_id : 111,
name : Jesus,
roleId : 1
},
{
_id : 222,
name : BatMan,
roleId : 2
},
{
_id : 333,
name : Jack,
roleId : 3
}
When user logs in and sending user object back to client, make sure to replace roleId with corresponding role object from DB.
Coming to code on Node JS :
By completely understanding your usecase we can divide them into following methods -
CreateUser
CreateTodo
DeleteTodo
ReadTodo
UpdateTodo
CommentTodo
AssignTodo
Lets go step by step, CreateUser.
Routes code snippet :
app.all('/users', users.requiresLogin);
// Users Routes
app.route('/users')
.post(users.hasPerms('globalPerms'), users.create);
In your Controller you can validate based on the input globalPerms, if validated allow to create user by calling next() else return with corresponding error message.
Now CreateTodo && DeleteTodo :
Both of them pretty much work on same logic with a small trick.
Routes code snippet :
app.all('/todos', users.requiresLogin);
// Users Routes
app.route('/todos')
.post(users.hasPerms('globalPerms','privatePerms'), todos.create);
.delete(users.hasPerms('globalPerms','privatePerms'), todos.delete);
For creating a Todo, globalPerms are with GOD & privatePerms are with SUPER HERO, both of them can be allowed.
Trick here will be in todos.delete method, just ensure user.id === todos.createById else SUPER HERO may go on to delete Todos created by GOD.
ReadTodo :
When a TODO is created it should have a createById stored likewise when a TODO is assigned to someone then assignedTo and assignedBy should be recorded too.
This makes lot of other operations easy to handle.
user.role.globalPerms - give GOD all TODO's data.
user.role.privatePerms - give TODO's either createdBy him/her or assigned to him/her.
user.role.globalPerms === undefined && user.role.privatePerms === undefined - its MAN and give TODO's which are only assignedTo him.
UpdateTodo & CommentTodo :
This is exact replica of what ReadTODO does so DIY
Last one, AssignTodo :
Simple one, loggedInUser.id === todos.createdById then he can assign it to anyone.
Two things to keep in mind here :
As assigning part mostly happens on your UI (Angular) front, i have given that approach of checking loggedInUser.id === todos.createdById. Logged in user any ways will see all TODO's by read operation and can assign it to anyone he/she likes.
Make sure a SUPER HERO can only assign a TODO to himself or other SUPER HERO or to a MAN but not to GOD. How you show Assign to options on UI front is out of scope of this question. This is just a heads up.
Hope this was clear.
NOTE : There was no necessity to give permissions to MAN in Roles collection & we managed all possible operations with out that.
This is a very broad question which can be solved in many ways.
You have added that you are using MEAN stack therefore I'll restrict my question to that.
One thing that you haven't included in the whole question is what kind of authentication architecture are you using. Let's say you are using token based authentication, generally people these days use it.
We have 3 types of users.
You have different options available to differentiate between type of tokens as well.
Different Collection (mongoDB) or Redis sets where they will be stored
The encrypted token will have type of the user as well etc.. (This will come in handy if you don't need to store tokens on the backend, you can just decrypt and check)
It will completely depend on use case.
Now, before allowing any user's entry to user specific route make sure that you are checking the token first.
Example
app.post('/godlevelroute', godtokencheck, callrouteandfunction);
app.post('/superherolevelroute', superheroroute, callrouteandfunction);
You must send token in header from angular and then you can take the data out from the header and then you can check if that specific user has permission to go through that route or not.
Let's say a god level user is logged in then he'll have the godleveltoken with him and we'll check that first before allowing him to access that route or else you can just show error message.
This can be your sample token checking function on server end
function checkToken(req, res, next) {
var token = req.headers['accesstoken']; //access token from header
//now depending upon which system you are following you can run a check
};
Node Module Suggestion : https://www.npmjs.com/package/jsonwebtoken
Now coming to frontend part. You are using angular based on what you have written, you can intercept the token before showing any page.
You can go through this blog to get a pictorial representation of what I have tried to explain. Click Here
Possible approach-> have role embedded in user collection/schema:
users document shall have the following:
{
_id : "email#mail.com",
name: "lorem ipsum",
role: "MAN"
}
As far as your post describes, only god can make and assign TODOs.
Roles Collection may hold the following:
{
_id : "MAN",
globalPerm: [],
privatePerm: [],
assignedPerm: ["r","u"],
},
{
_id : "SUPER_HERO",
globalPerm: [],
privatePerm: ["c","r","u","d"],
assignedPerm: ["c","r","u","d"],
},
{
_id : "GOD",
globalPerm: ["c","r","u","d"],
privatePerm: ["c","r","u","d"],
assignedPerm: ["c","r","u","d"],
}
Node JS Middlewares
After getting correct permission values for a user, you might want to use middlewares.
Sample express HTTP request route:
app.post('/updateTodo', permissions.check('privatePerm', 'c'), function (req, res) {
// do stuff
};
permissions.check is called before actually executing function body to update TODO.
Hence if a user tries to update a todo, it will first verify the corresponding permissions.

Chat application how to generate an id for a particular chat?

Let's say I have two users, "Matt" & "Kevin". Matt wants to message Kevin, by clicking a chat button to send Kevin a direct message a chat box boots up, he sends a message and Kevin receives it.
I generate the chat id by taking the person who sent it (Matt) and the person who received the message (Kevin) and concatenating it into an id.
var me = "Matt";
var user = "Kevin";
var uniqueChatID = me+user;
As I save the message server side (with mongoDB) the message object has a chatID of MattKevin. So now when I want to get back to that chat I can pull in all messages with the chatID of MattKevin.
This works fine, until Kevin wants to boot up a chat with Matt, then the id becomes KevinMatt. Now I am referencing a different chat, it's backwards. So If I want to pass uniqueChatID to get the messages it will pull a different set.
var me = "Kevin";
var user = "Matt";
var uniqueChatID = me+user;
So I am curious how can I set this up a bit better so that my program knows, ok Matt and Kevin have a chat, so if Matt messages Kevin it pulls in their chat or visa versa, Kevin messages Matt and it gets the same messages?
Sort them alphabetically:
var me = "Kevin";
var user = "Matt";
var uniqueChatID = [me, user].sort().join('');
That said, while this technically works, I'd recommend you do a little housekeeping - ensure they're always lowercase, and ensure on your db that you enforce unique usernames. Or, I'd even suggest giving the user a unique identifier (like a UUID) and use that instead to create the UCID:
var me = CurrentUser.uuid(); // 8cb3ebb8-30f9-11e5-a151-feff819cdc9f
var targetUser = Chat.targetUser(); // Matt: 9bc1ef9c-6719-4041-afd3-c5b87c90690d
var uniqueChatID = [me, targetUser].sort().join(',');
// 8cb3ebb8-30f9-11e5-a151-feff819cdc9f,9bc1ef9c-6719-4041-afd3-c5b87c90690d
And lastly, if your db supports relationships or connections, your best option is to separate chat table/collection for each chat and "connect" (or create a relationship) between both users and the chat. Then the next time you go and load it up, the connection will lead you to a unique chat that's connected to both users.
I think you approach is too complex. Furthermore, it looks like you want to embed the individual chat messages into the document bearing the created _id. The problem here is that there is a 16 MB size limit on BSON documents at the time of this writing. Upon reaching this limit, your users simply could not communicate any more. Increasing the size of documents may also lead to frequent document relocations, which is a very costly operation unless you use the new WiredTiger storage engine introduced in version 3.0 of MongoDB.
So we need a more scalable approach.
Here is how I would do it:
User:
{
_id: "Kevin",
email: "kevin#example.com"
/* Put further user details as you see fit*/
}
Message:
{
_id: new ObjectId(),
from: "Kevin",
/* You might want to have multi-person chats, hence the array */
to: ["Matt"],
ts: new ISODate(),
message: "Hi, Matt!"
}
Index:
db.messages.ensureIndex({from:1,to:1,ts:1})
Query for reconstructing all messages a user received:
var user = "Matt"
db.messages.find({"to": user}).sort({ts:1})
Now you can iterate over the result set and open a chat window for each "from" you find.
Query for reconstructing a defined chat
var user = "Matt"
var sender = "Kevin"
db.messages.find({"from": sender, "to":user}).sort({ts:1})
will give you all messages sent to Matt by Kevin, ordered by time. Since both queries should utilize the index, they should be pretty fast. You can use .limit(x) to query only the last x messages sent to user.
With this approach, you don't need an artificial _id, the index created allows you to do every query related to the participants efficiently and the messages can be sorted in order. Because each message is saved individually and does not change any more, you can store an almost indefinite number of messages and bypass the document relocation problem.

How to secure blueprint access in Sails.js

So I'm pretty new to Sails.js but it seems to bring a lot to the table like these blue prints,
Right now I have 2 models, Accounts / Friends
It is using association as Accounts has many Friends
On my client side I have a way to send notifications to users, one being a friends request so that works fine, it gives the user an option to accept or decline the invite
Here's the code:
socket.on('accounts', function(data) {
if (data.verb === "addedTo" && data.attribute === "notifications") {
socket.get('/notifications/' + data.addedId, function(note) {
console.log(note);
if (!$.isEmptyObject(note)) {
$scope.notifications.push({
'text': note.from_name + ' sent you a friends request',
'id': note.id,
'type': note.type,
'from_id': note.from_id
})
$.notify('You have a new notification', 'info');
$scope.$digest();
}
})
}
})
$scope.accept = function(notificationId, fromId) {
}
When people press the Accept button, I want to add a friend for both the person that accepted the notification, and the person that sent it so that it's mutual and they're both friends with each other.
I could simply do that by making a data object and doing socket.post to friends for both of them, and then doing a socket.put to update the notification as being read and deal away with it
The only problem is, I don't want people to come in and open JS console and spam socket.post a million friends to people maliciously
var obj {
'friend_name': 'Bill',
'online': 1
}
socket.post('/accounts/1/friends', obj);
So, how can I secure this so that people accessing post to /accounts/1/friends is only people that should be accessing this?
If I'm not making sense just ask. Thanks!
Example of being spammed:
somebody opens JS console and goes
var obj = { friend_name: 'bill', owner: 1 }; io.socket.post('/friends', obj);
500 times now the account with id 1 has 500 friends named bill
Correct me if I misunderstood something but I think your logic is wrong.
I assume Bill is also an account, but when you add only his name you don't have any connection with his account information and also can identify him only by a name. In order to have some kind of validation you will need an unique identifier of Bill. Instead of name you can add account_id and owner.
So if there are 3 accounts:
Chris
Bill
Joe
You will add var obj = { account_id: 2, owner: 1 };
So that way you can check with policies if friend with account_id and owner already exists before adding a new friend. If you have any questions about how to use policies just ask.
Anyway I think a better approach will be to have only one model and a many to many relationship with that model alone. Account will have a many to many relationship with itself through a third table that Sails will create (that will be your friends table). That way in the new table you will have two columns that will contain account ids. If let's say on the first row you have ids 1 and 2, that means Chris and Bill are both friends and you can think of some logic to make sure the rows are always unique so that you don't have problem with spam. Just for the record I haven't done many to many relationship with one model so you have to figure it out how it's done.

Categories

Resources