Efficient way making "Like" function without duplication? - javascript

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.

Related

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.

Best way to manage Chat channels in Firebase

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)

Which is worse? One more db acess or one outdated value?

PROBLEM
I'm running an application with AngularJS, Node JS, Express and MongoDB. I'm acessing MongoDB trought mongoose. My problem is that I have a lists of swords, each forged by someone. But, to access someone's profile, I need to use the ID of someone. The ID is unique. But I can't display the link as something link '352384b685326vyad6'. So, when someone create's a sword, I will store his or her name within the sword info.
To display a list of swords, for example, I could do something like this:
<div ng-repeat='sword in swords'>
<p> Sword name: {{sword.name}} </p>
<p> Author: <a href='#/user/{{sword.createdByID}}'> {{sword.createdByName}} </a> </p>
</div>
But, if the User changes his name, the sword will not update his creator name accordingly. What should I do? I have thought of some solutions, but I don't know which and if any could solve this in a good manner.
When someone changes own name, I could make a POST request with the new username and the ID, updating all sword that has createdByID equals to user ID. But I see this too strange.
SwordModel.find({ createdByID: req.body.id}, [...]);
When loading the swords in the controller via GET request, make another GET request for each sword and update sword.username based on the sword.createdById.
UserModel.findById(req.body.id), [...]);
Forget UX and use ugly links.
I want to know how can I maintain the username of each sword updated without affecting too much my DB Thanks for any advice.
MODELS - Just for reference.
sword.js
var mongoose = require('mongoose');
var SwordModel = mongoose.model('SwordModel',
{
name: String, //Sword's name
createdById: String, //ID of the user who created.
createdByName: String //Name of the user
});
module.exports = mongoose.model('SwordModel', SwordModel);
user.js
var mongoose = require('mongoose');
var UserModel = mongoose.model('UserModel',
{
name: String, //Name of the user.
ID: String //ID of the user.
});
module.exports = mongoose.model('UserModel', UserModel);
You can do it by referencing the User inside Swords if you make use of Mongoose schema.
After you make a reference to another schema you can use populate method to get the desired results.
Example (May be not exactly, but something similar to following) :
Sword Schema:
var swordSchema = new Schema({
name: String,
createdBy: {type: mongoose.Schema.Types.ObjectId, ref: 'User'}
});
Make model using the schema :
var swordModel = mongoose.model('Sword', swordSchema);
Find what you are looking for using populate.
See full documentation for populate here - http://mongoosejs.com/docs/populate.html
EDIT: note that I am recommending to keep user's name only in User model and just reference it.
I think we have less of a technical problem and more of a conceptual problem here.
Constraints
I take it for granted that...
users may change their usernames
usernames are unique
you only need to provide a link to a user, displayed by name
Furthermore, I will use plain JSON and MongoDB and trust that you can translate this to Mongoose.
The solution
Albeit users can change their usernames, this is not going to happen often. The more common use case is that you need to link to a username. So we first need to find out how to efficiently deal with that use case.
Since you only need the name of the smith for a given sword, there is nothing wrong with a sword model like
{
_id: new ObjectId(),
name: "Libertas",
smith: "Foobar"
}
to efficiently find "Foobar" in the users collection, we simply add an index here (if not already done):
db.users.createIndex({username:1}, {unique:true})
and your service can query efficiently by using
db.users.find({name: "Foobar"})
No need to save the _id of the user within the sword document, but still you can query for it efficiently.
Dealing with a change in the username is a rather rarely executed use case, so optimizing here does not make sense. However, if a user changes his or her username, your service can easily achieve that through
db.swords.update(
{ smith:"Foobar" },
{ $set:{ smith: "CoolNewUsername" },
{ multi: true, writeConcern: { w:1, j:true }
)
The last line of the above needs a little explanation. The multi: true option tells MongoDB to change all documents matching {smith: "Foobar"}, not only the first one found, that's easy to understand. But why to set the write concern to journaled? The first reason for it is that regardless of the write concern configured for the connection (which may even be unacknowledged), we need those changes to be durable. However, we usually do not need to have the changes to be propagated to more replica set members, so the chosen write concern gives you the best performance while still you can be sure that the changes were synced to a disk. If you need higher durability, of course you can set the write concern to {w:2} or {w:"majority"}.
Advantages
For the most common use case of this relationship (displaying a link to the user who forged a given sword), all information needed to do this is included in the sword's document, preventing possibly unnecessary queries.
Still, the smith of a given sword can be queried efficiently if a user clicks said link.
Changing a users name is possible and can be achieved pretty efficiently and durable
Disadvantages
The main disadvantage here is that you actually have to modify all affected swords when a user changes his or her username, whereas with a Mongoose reference that would be unnecessary. However, since this is a rare use case and using populate would result in the whole user document to be loaded where only the users name is needed, I see this disadvantage as negligible. Basically you are trading to cut down the queries needed for a common use case by half against the need for a manual update which occurs rather rarely.
I fail to see any other disadvantage.

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.

Getting existing Embedded Documents from Mongoose

I am trying to cache friends from social media in the user's doc and am having issues storing/getting them back.
First, I blindly strip the their friends cache and would just fill it with my fresh fetch of the data, but through that I found out that the embedded Documents must be unique among all the users (I have a couple of friends that are the same in my test accounts and I get:
{"name":"MongoError","err":"E11000 duplicate key error index: test-dev.users.$friends.outsideID_1 dup key: { : \"1205314313242\" }","code":11001,"n":0,"connectionId":274,"ok":1}
so from this I know that because that embedded document exists in my other already registered and updated account it can't create that document (outsideID is index because I plan on searching using it later))
So then I started trying to OutsideFriend.find({'outsideID':{$in:friendsIDs}}(where friendsIDs is an array of the ids I got from the SM query), etc.. and then in the callback I go through and try added the new friends and just add the existing docs to the user in question so that it's not trying to duplicate it in the system. However, for whatever reason OutsideFrind.find() never returns any docs... which makes me think that the embedded docs aren't centralized... but then why would the first attempt fail?
How am I supposed to do this? (schema below, please let me know if you need any other info!)
current schema:
//External friend model
var OutsideFriend = new Schema({
outsideID:{type:String, index:true, unique:true}
,username:String
,location:String
,avatarURL:String
});
//User model
var User = new Schema({
avatarURL:String
,mySMID:String
//Social Media info
,smID:{type:String, index:true, unique:true}
,tok:String
,tokSec:String
,friends:[OutsideFriend]
};
There's a lot going on in your question, but if you want to query OutsideFriend independent of User, then you shouldn't be embedding them in the User model. Instead, just store ObjectId references to the friends and then populate that in User when you need the full details. Don't prematurely optimize with embedded copies unless you find you need to as that introduces consistency challenges.
So your User model would change to:
var User = new Schema({
avatarURL:String
//Social Media info
,smID:{type:String, index:true, unique:true}
,tok:String
,tokSec:String
,friends:[{type: ObjectId, ref: 'OutsideFriend'}]
};

Categories

Resources