I have a question about best practice and how to add user authorization functionality. Should it be in model, controller or elsewhere.
Currently,
I have been building validation functions within my Mongoose Models
I have been building authentication/authorization checks using middleware and called from my routes.
My current challenge is when an an authenticated and authorized user attempts to update a model for which they are NOT the owner.
My authenticated user has been attached to my request, but that data is not going to be available from within the Mongoose Model so I am thinking that I should probably create some sort of validation function on the model that can be called from my controller, so that my logic lives nicely with the model but can be called from the controller.
Controller
exports.create = function (req, res) {
try {
if (!_.isEmpty(req.body.entity.ordererAccountId) && !_.isEqual(req.user.accountId.toString(), req.body.entity.ordererAccountId)) {
var err = mong.formatError({ message: 'Invalid Account Access' });
return res.status(403).json(err);
}
OrderSchema.create(req.body.entity, function (err, entity) {
if (err) {
return mong.handleError(res, err);
}
return res.status(201).json(mong.formatSuccess(entity));
});
} catch (e) {
console.log(e);
}
};
Model
'use strict';
// ------------------------------------------------------------
// Order Model
// ------------------------------------------------------------
var mongoose = require('mongoose');
var findOneOrCreate = require('mongoose-find-one-or-create');
var Schema = mongoose.Schema;
var OrderSchema = new Schema({
created_at: { type: Date },
updated_at: { type: Date },
ordererAccountId:
{
type: Schema.ObjectId, ref: 'Account',
required: true
},
supplierAccountId:
{
type: Schema.ObjectId, ref: 'Account'
},
userId:
{
type: Schema.ObjectId, ref: 'User',
required: true
},
status: {
type: String,
enum: ['Open', 'Sent'],
default: 'Open'
},
notes: String,
supplierCompanyName: String,
supplierEmail: String,
supplierContactName: String,
supplierPhone1: String,
supplierPhone2: String,
deliveryCompanyName: String,
deliveryEmail: String,
deliveryFirstName: String,
deliveryLastName: String,
deliveryAddress1: String,
deliveryAddress2: String,
deliveryCity: String,
deliveryState: String,
deliveryPostCode: String,
deliveryCountry: String,
deliveryPhone1: String,
deliveryPhone2: String,
});
OrderSchema.plugin(findOneOrCreate);
// ------------------------------------------------------------
// Validations
// ------------------------------------------------------------
// Validate only one open order at a time per user
OrderSchema
.path('status')
.validate(function (status, respond) {
var Order = mongoose.model('Order');
// Excluding this Order, make sure there are NO other orders for this user with the status of 'Open'
var condition = {
userId: this.userId,
status: 'Open',
_id: { $ne: this._id }
};
Order.count(condition, function (err, count) {
if (err) {
console.log(err);
}
else {
respond(count === 0);
}
});
}, 'There can be only one open order at a time.');
// ------------------------------------------------------------
// Pre-Save Hook
// ------------------------------------------------------------
OrderSchema.pre('save', function (next) {
var now = new Date().getTime();
this.updated_at = now;
if (!this.created_at) {
this.created_at = now;
}
next();
});
module.exports = mongoose.model('Order', OrderSchema);
you can use your "create" function as a validation middleware in your router,by doing something like this:
app.post('/yourRoute', create, function(req, res) {
//if validation success
//do somthing
});
not forgetting to pass the "next" function as a third argument to your create function
Related
When I try to update or findOneAndUpdate it only validates wether any required are missing, but it doesn´t validate the data type. At the moment I can update the name property to a number and no validation error happens. Any idea on what to do?
mongoose.set('runValidators', true);
const mongoose = require("mongoose")
const { Schema } = mongoose;
const UserSchema = new Schema ({
nome: {
type: String,
required: true,
trim: true
},
email: {
type: String,
required: true,
trim: true
},
morada: {
type: String,
required: true,
trim: true
}
});
const User = mongoose.model('User', UserSchema);
module.exports = User
const update = async (req, res) => {
try {
let post = await Post.findOneAndUpdate(req.params, req.body, {new: true});
res.json(post)
} catch (e) {
res.status(500).json(e)
}
}
As I have read here I could use the pre middleware to validate data type, but I´m having trouble with how to use it to update:
Post.pre("save", function(next, done) {
let self = this;
if (invalid) { // This is where I think I should validate the data type, but don´t know how to
// Throw an Error
self.invalidate("nome", "name must be a string");
next(new Error("nome must be a string"));
}
next();
});
Any suggestions? Thanks on advance
I try to call a related list of logs for a certain user via Mongoose populate. Who can help me with finishing the response?
These are the schemes:
const logSchema = new Schema({
logTitle: String,
createdOn:
{ type: Date, 'default': Date.now },
postedBy: {
type: mongoose.Schema.Types.ObjectId, ref: 'User'}
});
const userSchema = new Schema({
firstName: {
type: String,
required: true
},
lastName: {
type: String,
required: true
}
logs: { type: mongoose.Schema.Types.ObjectId, ref: 'logs' }
});
mongoose.model('User', userSchema);
mongoose.model('logs', logSchema);
Inspired by the Mongoose documentary (see above) and other questions in relation to this subject I think I got pretty far in making a nice get. request for this user. I miss the expierence to 'translate it' to Express.
const userReadLogs = function (req, res) {
if (req.params && req.params.userid) {
User1
.findById(req.params.userid)
.populate('logs')
.exec((err, user) => {
if (!user) { }); // shortened
return;
} else if (err) {
return; // shortened
}
response = { //question
log: {
user: user.logs
}
};
res
.status(200)
.json(response);
});
} else { }); //
}
};
The response in Postman etc would be something like this:
{
"log": {5a57b2e6f633ce1148350e29: logTitle1,
6a57b2e6f633ce1148350e32: newsPaper44,
51757b2e6f633ce1148350e29: logTitle3
}
First off, logs will not be a list of logs; it will be an object. If you want multiple logs for each user, you will need to store is as an array: logs: [{ type: mongoose.Schema.Types.ObjectId, ref: 'logs' }]
From the Mongoose docs: "Populated paths are no longer set to their original _id , their value is replaced with the mongoose document returned from the database by performing a separate query before returning the results." In other words, in your query user.logs will be the logs document for each user. It will contain all the properties, in your case logTitle, createdOn, and postedBy.
Sending user.logs as json from the server is as easy as: res.json(user.logs). So your query can look like this:
const userReadLogs = function (req, res) {
if (req.params && req.params.userid) {
User1
.findById(req.params.userid)
.populate('logs')
.exec((err, user) => {
if (!user) { }); // shortened
return;
} else if (err) {
return; // shortened
}
res.status(200).json(user.logs)
});
} else { }); //
}
};
I hope this makes it a little bit clearer!
So I've been working on a project and I finished most of it, but then this error popped up, saying there is something that is undefined, here is the error:
E11000 duplicate key error index: build-a-voting-app.polls.$votedIp_1 dup key: { : undefined }
Here is my code for my create new mongo schema file (polls.model.js)
const mongoose = require('mongoose');
const Schema = mongoose.Schema;
const pollSchema = new Schema({
title: { type: String, unique: true, required: true },
choices: [
{
title: { type: String, required: true },
count: { type: Number, default: 0 }
}
],
votedIp: [{ type: String, unique: true }],
createdAt: {type:Date, default:Date.now()},
createdBy: String
});
const Poll = mongoose.model('polls', pollSchema);
module.exports = Poll;
Here is the function where I add the inputs
function submitVote(field, res, ip) {
Poll.findOneAndUpdate(
{ choices: { $elemMatch: { title: field } } },
{ $inc: { 'choices.$.count': 1 }, $addToSet: { 'votedIp': ip } },
{ new: true },
function (err, poll) {
if (err) throw err;
res.json({ updated: poll });
}
);
}
Here is how I first created it
var newPoll = new Poll({
title: req.body.title,
choices: choicesArr,
createdBy: req.session.user.username || req.session.user
}).save(function (err, poll) {
if (err) throw err
res.redirect('/mypolls')
});
If you want to see the full code please go to https://github.com/ElisaLuo/Freecodecamp-Build-A-Voting-App
I'm using the ip addresses for checking if the user has voted or not (I'm building a voting app), but right now, I cannot even create a new schema / poll. Does anyone know why the error happens and how I can solve it?
#Elisa l - you may want to read this - mongoose enforce unique attribute on subdocument property
However, I did manage to test with mongoose-mock and the behavior is as expected - test results below (please do check the two versions of votedIp in the test code snippets)
and as described in the MongoDb document referenced in the above link. Mongoose does not enforce the unique integrity, MongoDb does.
With the mocha test below (inserted as snippets, not to run the code but just for better readability, please ignore the messy look of the comments in the code but the permutation and combination had to be worked out!), I did manage to create the mongoose schema by adding a create method in "Poll". please note the change in the schema - votedIp: { type: String, unique: true }, you can change it to array in the test code.
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var pollSchema = new Schema({
title: { type: String, unique: true, required: true },
choices: [
{
title: { type: String, required: true },
count: { type: Number, default: 0 }
}
],
votedIp: { type: String, unique: true },
createdAt: {type:Date, default:Date.now()},
createdBy: String
});
// Below code added by RJv (ie. me :)
var NewPoll = new mongoose.Schema(pollSchema);
NewPoll.statics.create = function (params, callback) {
var newUpdate = new NewPoll(params);
newUpdate.save(function(err, result) {
callback(err, result);
});
return newUpdate;
};
var Poll = mongoose.model('Model', NewPoll);
module.exports = Poll;
var expect = require('chai').expect,mongooseMock = require('mongoose-mock'),proxyquire=require('proxyquire'),
sinon = require('sinon'), chai=require('chai'),sinonChai = require("sinon-chai");chai.use(sinonChai);
var Poll;
before(function(done){
Poll = proxyquire('./Poll', {'mongoose': mongooseMock});
done();
})
describe('model', function() {
/* beforeEach(function (done) {
Poll = proxyquire('./Poll', {'mongoose': mongooseMock});
done();
});*/
it("should be called once",function(done){
setTimeout(done, 15000);
var callback = sinon.spy();
var poll1 = Poll.create({ "title": 'jv', "choices": [{"title":"jv#gmail.com","count":"1"}],
"votedIp":"9.1.2.1","createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
// Below to pass data for votedIp as an array as described in the original schema by Elisa
//"votedIp":[{"add1":"9.","add2":"1.","add3":"2.","add4":"1"}],"createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
//expect(poll1.votedIp[0].add1+poll1.votedIp[0].add2+poll1.votedIp[0].add3+poll1.votedIp[0].add4).equals("9.1.2.1");
expect(poll1.save).calledOnce;
console.log(JSON.stringify(poll1));
expect(poll1.votedIp).equals("9.1.2.1");
done();
});
it('should expect same ip to get added', function(done) {
this.timeout(5000);
setTimeout(done, 15000);
var callback = sinon.spy();//mock(new Poll({ title: 'jv', choices: [{title:"jv#gmail.com","count":"1"}], votedIp:[{ad1:"9.",add2:"1.",add3:"2.",add4:"1"}],createdAt:"25/07/2017",createdBy:"Lisa"}));
var poll = Poll.create({ "title": 'jv', "choices": [{"title":"jv#gmail.com","count":"1"}],
"votedIp":"9.1.2.1","createdAt":"23/07/2017","createdBy":"Lisa"}, callback);
// var poll = Poll.create({ "title": 'jv', "choices": [{"title":"jv#gmail.com","count":"1"}],
// Below to pass data for votedIp as an array as described in the original schema by Elisa
// "votedIp":[{"add1":"9.","add2":"1.","add3":"2.","add4":"1"}],"createdAt":"25/07/2017","createdBy":"Lisa"}, callback);
// expect(poll.votedIp[0].add1+poll.votedIp[0].add2+poll.votedIp[0].add3+poll.votedIp[0].add4).equals("9.1.2.1");
expect(poll.save).calledOnce;
expect(poll.votedIp).equals("9.1.2.1");
//assert(spy.calledOnce);
done();
});
});
Are you calling submitVote multiple times in quick succession? You might be running into https://jira.mongodb.org/browse/SERVER-14322.
The suggested fix for this is to check the error and if one of the calls fails retry it.
https://docs.mongodb.com/manual/reference/method/db.collection.update/#use-unique-indexes
I have the following model for mongoose.model('quotes'):
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var quotesSchema = new Schema({
created: { type: String, default: moment().format() },
type: { type: Number, default: 0 },
number: { type: Number, required: true },
title: { type: String, required: true, trim: true},
background: { type: String, required: true },
points: { type: Number, default: 1 },
status: { type: Number, default: 0 },
owner: { type: String, default: "anon" }
});
var settingsSchema = new Schema({
nextQuoteNumber: { type: Number, default: 1 }
});
// Save Setting Model earlier to use it below
mongoose.model('settings', settingsSchema);
var Setting = mongoose.model('settings');
quotesSchema.pre('save', true, function(next) {
Setting.findByIdAndUpdate(currentSettingsId, { $inc: { nextQuoteNumber: 1 } }, function (err, settings) {
if (err) { console.log(err) };
this.number = settings.nextQuoteNumber - 1; // substract 1 because I need the 'current' sequence number, not the next
next();
});
});
mongoose.model('quotes', quotesSchema);
There is an additional Schema for mongoose.model('settings') to store an incrementing number for the incrementing unique index Quote.number im trying to establish. Before each save, quotesSchema.pre('save') is called to read, increase and pass the nextQuoteNumber as this.number to the respectively next() function.
However, this entire .pre('save') function does not seem to trigger when saving a Quote elsewhere. Mongoose aborts the save since number is required but not defined and no console.log() i write into the function ever outputs anything.
Use pre('validate') instead of pre('save') to set the value for the required field. Mongoose validates documents before saving, therefore your save middleware won't be called if there are validation errors. Switching the middleware from save to validate will make your function set the number field before it is validated.
quotesSchema.pre('validate', true, function(next) {
Setting.findByIdAndUpdate(currentSettingsId, { $inc: { nextQuoteNumber: 1 } }, function (err, settings) {
if (err) { console.log(err) };
this.number = settings.nextQuoteNumber - 1; // substract 1 because I need the 'current' sequence number, not the next
next();
});
});
For people who are redirected here by Google, make sure you are calling mongoose.model() AFTER methods and hooks declaration.
In some cases we can use
UserSchema.pre<User>(/^(updateOne|save|findOneAndUpdate)/, function (next) {
But i'm using "this", inside the function to get data, and not works with findOneAndUpdate trigger
I needed to use
async update (id: string, doc: Partial<UserProps>): Promise<User | null> {
const result = await this.userModel.findById(id)
Object.assign(result, doc)
await result?.save()
return result
}
Instead of
async update (id: string, doc: Partial<UserProps>): Promise<User | null> {
const result = await this.userModel.findByIdAndUpdate(id, doc, { new: true, useFindAndModify: false })
return result
}
The short solution is use findOne and save
const user = await User.findOne({ email: email });
user.password = "my new passord";
await user.save();
I ran into a situation where pre('validate') was not helping, hence I used pre('save'). I read that some of the operations are executed directly on the database and hence mongoose middleware will not be called. I changed my route endpoint which will trigger .pre('save'). I took Lodash to parse through the body and update only the field that is passed to the server.
router.post("/", async function(req, res, next){
try{
const body = req.body;
const doc = await MyModel.findById(body._id);
_.forEach(body, function(value, key) {
doc[key] = value;
});
doc.save().then( doc => {
res.status(200);
res.send(doc);
res.end();
});
}catch (err) {
res.status(500);
res.send({error: err.message});
res.end();
}
});
Mongo Populate
Im trying to populate some user info onto the articles query
exports.articleByID = function(req, res, next, id) {
Article.findById(id).populate('user', 'displayName', 'email').exec(function(err, article) {
if (err) return next(err);
if (!article) return next(new Error('Failed to load article ' + id));
req.article = article;
next();
});
};
Im getting the error
MissingSchemaError: Schema hasn't been registered for model "email".
Any ideas??
Here is the schema
'use strict';
/**
* Module dependencies.
*/
var mongoose = require('mongoose'),
Schema = mongoose.Schema;
/**
* Article Schema
*/
var ArticleSchema = new Schema({
created: {
type: Date,
default: Date.now
},
title: {
type: String,
default: '',
trim: true,
required: 'Title cannot be blank'
},
content: {
type: String,
default: '',
trim: true
},
user: {
type: Schema.ObjectId,
ref: 'User'
}
});
mongoose.model('Article', ArticleSchema);
The third parameter to populate is the name of the model you wish to use for population, overriding what's specified in the schema.
Assuming email is a field you want from the user doc, include that in the second parameter instead:
exports.articleByID = function(req, res, next, id) {
Article.findById(id).populate('user', 'displayName email').exec(...