Proper way to add a friend route in node.js and mongoose? - node.js

I'm planning to to create a route where a user could add another user as his/her friend, so that they could chat to each other once they are friends.
So basically once User A has sent a request to User B, User B will get a live notification about the request via socket.io
The problem right now is that, I couldn't come up with my own solution on how to implement the above scenario, from what I know, I should create two routes GET and POST
I'm using mongoose for database query, insert , update and delete
Here's my code
// GET route for getting the user's information -- Simple route
router.get('/users/:facebook_name', function(req, res) {
User.findOne(facebook_name, function(err, user) {
if (!user) {
res.json({message: "Couldn't find a user by that name"});
return;
}
res.json(user);
});
});
// POST route for adding a friend
router.post('/friendships/create/:facebook_name', ensureAuthenticated, function(req, res) {
// What should i put in this route to make the adding a friend feature works?
User.findOneAndUpdate(facebook_name, function(err, user) {
if (user) {
res.json({message: "You already send a friend request to that person"});
return;
}
// Send a live notification to the other user
socket.emit('sending request', {message: "added you as a friend"});
});
});
user Schema code -- Not really sure about this one either
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = new Schema({
friends: [{ type: Schema.Types.ObjectId, ref: 'User'}],
facebook: {
id: String,
token: String,
// email: String,
displayName: String,
photo: String
}
});
// Should I even create this schema?
var FriendsRequest = new Schema({
madeBy: [{ type: Schema.Types.ObjectId, ref: 'User'}],
})
module.exports = mongoose.model('User', UserSchema);
module.exports = mongoose.model('FriendsRequest', FriendsRequest);
I'm not entirely honest with you guys, in the POST route, i have no freaking idea on how to write the logic, because I'm really confuse right now, how the User B gonna get the live request notification? Should i create another route for that?
This is my problem when it comes to building slightly complex apps , i just couldn't come up with a good logic on how to do a certain feature even though it looks pretty easy. I've been stuck in this problem for almost 4 hours, browsing and reading the net, but I believe SO is the only place for me to find a clue on how to do something.
Thank you.

What you can do is create socket for each facebookName(if unique).
On Client Side:
socket.on('connection', function (data) {
socket.emit('setFacebookName', facebookName); });
}
Server saves each socket with facebookName:
socket.on('setFacebookName', function (facebookName) {
users[facebookName]=socket;
});
Now, when user sends chat request to that user in this request
// POST route for adding a friend
router.post('/friendships/create/:facebook_name', ensureAuthenticated, function(req, res) {
// What should i put in this route to make the adding a friend feature works?
User.findOneAndUpdate(facebook_name, function(err, user) {
if (user) {
res.json({message: "You already send a friend request to that person"});
return;
}
// Send a live notification to the other user
sendLiveNotification(facebook_name);
});
});
function sendLiveNotification(facebookName){
socket.on('send notification', function (facebookName) {
users[facebookName].emit('sending request', "has sent friend request");
});
}

You're trying to get a two step process, so you will need at least two calls where one is a request from the requester, and the other is the decision whether or not to allow that request from the requestee. You can handle your callback for the first function utilizing a Boolean where if it's a new request the user could be prompted with a popup on the client.
A good purpose of Mongoose is the extensions to the Schema that you can make, so here I'm adding two functions: one from the requester requesting requestee's friendship, and the other the decision of the requestee
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var UserSchema = new Schema({
friendsAccepted: [{ type: Schema.Types.ObjectId, ref: 'User'}],
friendsRequested: [{ type: Schema.Types.ObjectId, ref: 'User'}],
friendsPending: [{ type: Schema.Types.ObjectId, ref: 'User'}],
friendsRejected: [{ type: Schema.Types.ObjectId, ref: 'User'}],
facebook: {
id: String,
token: String,
// email: String,
displayName: String,
photo: String
}
});
UserSchema.statics.requesterInitiatedRequestForFriendship = function(requesterID, requesteeID, cb) {
mongoose.model('UserSchema').findOne({_id: requesterID}).exec(function(err, requester) {
if (err) return cb(err);
mongoose.model('UserSchema').findOne({_id: requesteeID}).exec(function(err, requestee) {
if (err) return cb(err);
if (requestee.friendsAccepted(requesterID) === -1 &&
requestee.friendsRequested(requesterID) === -1 &&
requestee.friendsPending(requesterID) === -1 &&
requestee.friendsRejected(requesterID) === -1) {
requestee.friendsPending.push(requesterID);
requester.friendsRequested.push(requesterID);
requestee.save();
requester.save();
cb(null, true);
} else {
cb(null, false);
};
});
});
};
UserSchema.statics.requesteeDecidedOnFriendship = function(requesterID, requesteeID, allowed, cb) {
mongoose.model('UserSchema').findOne({_id: requesterID}).exec(function(err, requester) {
if (err) return cb(err);
mongoose.model('UserSchema').findOne({_id: requesteeID}).exec(function(err, requestee) {
if (err) return cb(err);
if ((requestee.friendsAccepted(requesterID) === -1 &&
requestee.friendsRequested(requesterID) === -1 &&
requestee.friendsPending(requesterID) > -1 &&
requestee.friendsRejected(requesterID) === -1) &&
requester.friendsRequested(requesteeID) > -1) {
requestee.friendsPending.forEach(function(uid, idx) {
if (uid === requesterID) {
requestee.friendsPending.splice(idx, 1);
return;
};
});
requester.friendsRequested.forEach(function(uid, idx) {
if (uid === requesteeID) {
requester.friendsRequested.splice(idx, 1);
return;
};
});
if (allowed) {
requestee.friendsAccepted.push(requesterID);
requester.friendsAccepted.push(requesteeID);
} else {
requestee.friendsRejected.push(requesterID);
requester.friendsRejected.push(requesteeID);
}
requestee.save();
requester.save();
};
cb(null);
});
});
}
module.exports = mongoose.model('User', UserSchema);
So a couple things happening:
hasn't been tested
it's not DRY
it's limited without an additional Friendship Schema
With a Friendship Schema, you can define levels of rejection (e.g. "not at this time", etc.), you can more well flush out details and granular control for the changing behavior of friendships. In the above, you can see that once you're rejected, it's pretty fatalistic in that it's determined at no time you shall become friends! So to get more of that type of behavior, I'd definitely go with a Friendship Schema with it's statics and methods flushed out, as should be users.

Related

how to stop users from viewing and updating another user's data in node.js?

I am storing a parking detail with a merchant id in the mongoose schema since a parking belongs to a certain merchant user and it cannot be empty or null.
Here is the model:
const parkingSchema = new mongoose.Schema({
merchantId: {
type: mongoose.Schema.Types.ObjectId,
required: true,
ref: "Merchant",
},
//other details
})
merchant model is something like this:
const merchantSchema = new mongoose.Schema({
merchantId: {
type: mongoose.Schema.Types.ObjectId,
ref: "Auth",
},
//other details
})
And finally the auth schema:
const authSchema = new mongoose.Schema({
accountType: {
type: String,
required: true,
trim: true,
default: "user",
enum: ["merchant", "user", "provider"],
},
//other details
})
If the original user wishes it, I simply want to update the parking data; otherwise, I want to throw an error.
I am using jsonwebtoken to authenticate users.
Here is the query to update the data:
exports.updateParking = async (req, res) => {
try {
const { parkingName, price, address, name, phoneNumber, about } = req.body;
const { parkingImage } = req.files;
const check_exist = await Auth.findById(req.data.id);
if (!check_exist) return res.status(404).json({ error: "User not found" });
console.log(req.data.id);
const updateData = await Parking.findByIdAndUpdate(
{ _id: req.params.id, merchantId: req.data.id }, // I think here is the problem
{
$set: {
parkingName,
price,
address,
...
},
}
);
return res.status(200).json({
success: true,
msg: "Parking has updated successfully",
});
} catch (error) {
return error.message;
}
};
However, the issue is that other users can now update another user's data which I want to stop
below is the query of middleware:
routing.patch("/parking/update/:id", middleware.authenticateToken, merchant.updateParking)
You should be showing each user only their parkings that they have created or belong to them.
const myParkings = async (req, res) => {
// always await code in try/catch block
const merchants = await Parkings.find({ user: req.user._id })
.. then populate the fields that you want to show
res.status(200).json({
success: true,
bookings,
});
};
you have to set this req.user._id when user logins. You could create a session.
I think what you're looking for is something like CASL Mongoose (or a similar package), and more specifically, the "conditions" section of the CASL docs.
What you're dealing with here is the distinction between 2 concepts:
AuthN (authentication) - determines who someone is and whether they are "authenticated" to make an API request
AuthZ (authorization) - determines what the authenticated user is allowed to do
In your app, middleware.authenticateToken is responsible for the AuthN piece of the equation. It makes sure that only users that have created an account are able to make requests to your API routes.
What you still need to solve for is the AuthZ piece, which can be done in a bunch of different ways, but one popular one is to use CASL, which is a Node AuthZ library that allows you to utilize your ORM's native query syntax to limit actions based on the authenticated (AuthN) user's attributes.
In other words, you can do something like, "Only allow user with ID 1 to update Parking entities that he/she owns". Below is generally what you're looking for (not tested for your use case, but the general idea is here):
const casl = require('#casl/ability');
// Define what a `Auth` (user) can do based on their database ID
function defineMerchantAbilities(merchantUser) {
const abilities = casl.defineAbility((allow, deny) => {
// Allow merchant to update a parking record that they own
allow('update', 'Parking', { merchantId: merchantUser.id })
})
return abilities
}
exports.updateParking = async (req, res) => {
const userId = req.data.id
const parkingId = req.params.id
// Find your merchant user in DB (see my comments at end of post)
const merchantUser = await Auth.findById(userId)
// Find your parking record
const parking = await Parking.findById(parkingId)
// Pass user to your ability function
const ability = defineMerchantAbilities(merchantUser)
// This will throw an error if a user who does not own this Parking record
// tries to update it
casl.ForbiddenError
.from(ability)
.throwUnlessCan('update', casl.subject('Parking', parking))
// If you make it here, you know this user is authorized to make the change
Parking.findByIdAndUpdate( ...your code here )
}
Additional comments/notes:
I would recommend removing your try/catch handler and using an Express default error handler as it will reduce the boilerplate you have to write for each route.
I would also recommend writing a middleware that finds a user by ID in the database and attaches it to a custom property called req.user so you always have req.user available to you in your authenticated routes.

Subdocument does not update - mongoose

I'm trying to update a subdocument of the parent document.
I have a document called "Post" and I reference the "User" document like this:
const PostSchema = new mongoose.Schema({
title: String,
user: {
type: mongoose.Schema.Types.ObjectId,
ref: 'User',
required: true
},
},
{ collection: 'posts' })
const Post = mongoose.model('Post', PostSchema);
module.exports = Post;
I'm trying to change the name, for example of whoever posted it. The name field is in "User".
I'm trying to change it this way:
exports.update = async (req, res) => {
//find user by its id, update its post with what's in req.body
Post.findById(req.body.id, function(err, result) {
console.log(result)
if (!err) {
if (!result){
res.status(404).send('User was not found');
}
else{
result.user.nome = "User Name";
result.markModified("user");
result.save(function(saveerr, saveresult) {
if (!saveerr) {
res.status(200).send(saveresult);
} else {
res.status(400).send(saveerr.message);
}
});
}
} else {
res.status(400).send(err.message);
}
}).populate("user");
}
This is my route.js
app.put(
"/api/produtor/update",
controller.update
);
When I run it on the postman, I get status 200 and the name appears modified in the return, but it is not saved in the bank.
I would appreciate it if someone could help me analyze it!
This might be outdated but as per this GitHub comment, this might not be possible out of the box.
I would suggest to update the User by using User schema instead of going through the Post Schema.

Accessing properties of object/cursor returned from .find and .forEach in mongodb with nodejs

changed schema and everything went crazy (see changes below). now accessing properties from .find() and cursor.forEach() is returning 'undefined' in backend:
EDIT: have found
.find().lean().exec(callback)
allows access to properties in callback but hard to do anything with them and that to access properties by doing
doc._doc.property
works in callbacks:
.find(function(err,doc){for (i in docs){doc=docs[i]; console.log(doc._doc.property)}}
and .forEach(function(doc){console.log(doc._doc.property)}:
My schema once looked like this
for collection of people
{
name: String,
v: Types.ObjectId, ref: V //shorthand
r: {
e: [{}],
u: [{}]
}
}
now it looks like this
var people = new mongoose.Schema (
{
name: String,
v: {type: mongoose.Schema.Types.ObjectId, ref: V}
r: {
e: [{type: mongoose.Schema.Types.ObjectId, ref: R}],
u: [{type: mongoose.Schema.Types.ObjectId, ref: R}]
}
}
)
mongoose.model('people',people);
for collection of r
var collR = new mongoose.Schema({}, {strict:false})
mongoose.model('R',collR)
nodejs controller 1:
module.exports.getProducts = function (req, res) {
people.find(req.query)
.populate('v r.e r.u')
.exec(function (err, data) {
if (err) {sendJsonResponse(res,400,err)}
else {
data.forEach(function(single){
single.r.e.forEach(function(sing){
console.log(sing) //defined, and i saw rating, and its defined
console.log(sing.rating); //undefined
// do something with sing.rating but it's undefined here
})
})
sendJsonResponse(res,200,data); //not undefined on frontend success callback
}
});
};
node controller 2:
module.exports.getProducts = function (req, res) {
people.find(req.query)
.populate('v r.e r.u')
.exec(function (err, data) {
if (err) {sendJsonResponse(res,400,err)}
else {
data.forEach(function(single){
R.find({person: single.name}, function (err, dat) {
dat.forEach(function(sing){
console.log(sing) //defined and rating defined
console.log(sing.rating); //undefined ugh.
//do something with rating but cant bc undefined here
})
})
})
//if i send data back here, in success callback, data[i].r.e[j].rating is defined for all i and j, whaaa!?!
}
});
};
one of the sing's logged from the cursor.forEach loop---
{_id: 1254357653, name: peep, rating: 6, type: some type}
EDIT:
ya so:
collection.find(query).exec(function(err,docs) {
docs.forEach(function(singleDoc) {
console.log(singleDoc._doc.property); //DEFINED, bad boyz 4 lyfe *_*
})
})
so i finally decided to console.log the darn keys of the document returned from a cursor.forEach
this also returns defined:
collection.find(query).lean().exec(function(err,docs) {
console.log(docs[i].property); //for all i, THEY'RE DEFINED!!!!! wooo
})
well now another issue pops up when i try to do an update inside a find
collection.find(query).exec(function(err,docs) {
if (err) {return errorHandler(err)};
var doc = docs[0];
var captainKeyes = Object.keys(req.body);
for (k = 0 ; k < captainKeyes.length ; k++) {
//update the doc key/value pairs with what is sent in req.body
doc._doc[captainKeyes[k]] = req.body[captainKeyes[k]];
//from above, learned to access properties captainKeyes[k], you have to first access
//the hidden property _doc to get to actual doc
}
doc.save()
//old doc is still in db, damn. and all this used to work before
//we added that R collection :(
})
I changed the schema for the collection R to have some keys, changing it from just an empty object with strict: false.
from {{},strict:false} to {{name: String, rating: Number, person: String},strict:false}
now i dont have to use _doc, wooohoooo, and all the queries works normally again.
moral of the story, i didn't really understand how to implement a schemaless collection properly, and then stuff got cray

How to check values against the DB when using the pre-save hook?

On a User schema, I'd like to check if the specified email already exists for the specified shop, before saving.
var UserSchema = new Schema({
_shop: {
type: Schema.Types.ObjectId,
ref: 'Shop',
required: true
},
email: String,
//...
});
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
// How to do use the static method isThatEmailFreeForThisShop here?
});
UserSchema.statics.isThatEmailFreeForThisShop = function(email, shop_id, cb) {
this.find({email: email, _shop: shop_id}, function(err, users) {
// ...
});
});
There could be different users with the same email as long as they are from different shops.
I do not know how to use the static method in the pre-save hook...
Thanks!
You've created a User Model instance somewhere (I'll call it User):
var User = mongoose.model('user', UserSchema);
So, the isThatEmailFreeForThisShop function is available on the User model:
User.isThatEmailFreeForThisShop(...)
From your save hook:
UserSchema.pre('save', function(next) {
if (!this.isNew) return next();
User.isThatEmailFreeForThisShop(this.email, this._shop,
function(err, result) {
if (result) { // found
// do something
return next({ error: "duplicate found" });
}
return next();
});
});
You may also want to switch to using the pre-validate rather than save.
I'd expect in your function, isThatEmailFreeForThisShop that you'd call the cb parameter when the results have been "found".
You probably would use findOne (reference) rather than find. Given that there's still a race condition, you'd want to add an index as a compound index email and shop_id and set the unique attribute to true to prevent duplicates from sneaking in (then, you'll need to handle the fact that a save on a model instance may throw an error.)
UserSchema.statics.isThatEmailFreeForThisShop = function(email, shop_id, cb) {
this.findOne({email: email, _shop: shop_id}, function(err, user) {
// ...
cb(err, user != null);
});
});

Automatically remove referencing objects on deletion in MongoDB

Let's suppose I have a schema like this:
var Person = new Schema({
name: String
});
var Assignment = new Schema({
name: String,
person: ObjectID
});
If I delete a person, there can still be orphaned assignments left that reference a person that does not exist, which creates extraneous clutter in the database.
Is there a simple way to ensure that when a person is deleted, all corresponding references to that person will also be deleted?
You can add your own 'remove' Mongoose middleware on the Person schema to remove that person from all other documents that reference it. In your middleware function, this is the Person document that's being removed.
Person.pre('remove', function(next) {
// Remove all the assignment docs that reference the removed person.
this.model('Assignment').remove({ person: this._id }, next);
});
If by "simple" you mean "built-in", then no. MongoDB is not a relational database after all. You need to implement your own cleaning mechanism.
The remove() method is deprecated.
So using 'remove' in your Mongoose middleware is probably not best practice anymore.
Mongoose has created updates to provide hooks for deleteMany() and deleteOne().
You can those instead.
Person.pre('deleteMany', function(next) {
var person = this;
person.model('Assignment').deleteOne({ person: person._id }, next);
});
In case if anyone looking for the pre hook but for deleteOne and deleteMany functions this is a solution that works for me:
const mongoose = require('mongoose');
...
const PersonSchema = new mongoose.Schema({
name: {type: String},
assignments: [{type: mongoose.Schema.Types.ObjectId, ref: 'Assignment'}]
});
mongoose.model('Person', PersonSchema);
....
const AssignmentSchema = new mongoose.Schema({
name: {type: String},
person: {type: mongoose.Schema.Types.ObjectId, ref: 'Person'}
});
mongoose.model('Assignment', AssignmentSchema)
...
PersonSchema.pre('deleteOne', function (next) {
const personId = this.getQuery()["_id"];
mongoose.model("Assignment").deleteMany({'person': personId}, function (err, result) {
if (err) {
console.log(`[error] ${err}`);
next(err);
} else {
console.log('success');
next();
}
});
});
Invoking deleteOne function somewhere in service:
try {
const deleted = await Person.deleteOne({_id: id});
} catch(e) {
console.error(`[error] ${e}`);
throw Error('Error occurred while deleting Person');
}
You can leave the document as is, even when the referenced person document is deleted. Mongodb clears references which point to non-existing documents, this doesn't happen immediately after deleting the referenced document. Instead, when you perform action on the document, e.g., update. Moreover, even if you query the database before the references are cleared, the return is empty, instead of null value.
Second option is to use $unset operator as shown below.
{ $unset: { person: "<person id>"} }
Note the use of person id to represent the value of the reference in the query.
you can use soft delete. Do not delete person from Person Collection instead use isDelete boolean flag to true.
Use $pull. Suppose you have a structure like this.
Stuff Collection:
_id: ObjectId('63dd23c633c17a718c4c5db7')
item: "Item 1"
user: ObjectID('63de669153bc12ecb9081b9e')
User collection:
_id: ObjectId('63de669153bc12ecb9081b9e')
stuff: array[ObjectId('63dd23c633c17a718c4c5db7'), ObjectId('63de3a69715ec134e161b0ea')]
Then after you remove the stuff:
const stuff = Stuff.findById(req.params.id)
const user = User.findById(req.params.id)
await stuff.remove()
// here you can use $pull to update
await user.updateOne({
$pull: {
stuff: stuff.id
}
})
you can simply call the model that needs to be deleted and delete that document like this:
PS: This answer is not specific to the question schema.
const Profiles = require('./profile');
userModal.pre('deleteOne', function (next) {
const userId = this.getQuery()['_id'];
try {
Profiles.deleteOne({ user: userId }, next);
} catch (err) {
next(err);
}
});
// in user delete route
exports.deleteParticularUser = async (req, res, next) => {
try {
await User.deleteOne({
_id: req.params.id,
});
return res.status(200).json('user deleted');
} catch (error) {
console.log(`error`, error);
return next(error);
}
};

Resources