I'm using Meteor methods to update documents so I can share them easier and have more control. However i've ran into a problem with checking ownership.
How should I check to make sure the user calling the update method is the owner of the document? Currently i'm grabbing the document first then running the update.
Is there a better pattern to accomplish this?
Meteor.methods({
'Listing.update': function(docId, data) {
var doc = db.listings.findOne({_id: docId}) || {};
if (doc.userId !== this.userId) {
throw new Meteor.Error(504, "You don't own post");
}
// ensure data is the type we expect
check(data, {
title: String,
desc: String
});
return db.listings.update(docId, {$set: data});
}
});
You don't need the additional db call to fetch the original doc, just make the userId an additional criteria in the update selector. If no doc exists with the correct _id and userId no update will be done. update returns the number of docs updated so it will return 1 on success and 0 on failure.
like this:
'Listing.update': function(docId, data) {
var self = this;
check(data, {
title: String,
desc: String
});
if ( ! self.userId )
throw new Meteor.Error(500, 'Must be logged in to update listing');
res = db.listings.update({_id: docId, userId: self.userId}, {$set: data});
if ( res === 0 )
throw new Meteor.Error( 504, "You do not own a post with that id" );
return res;
}
Also, if you use findOne to check a document's existence, use the fields option to limit what you return from the db. Usually just {fields: {_id:1}}.
Related
I have this piece of code in client side:
Tracker.autorun(function () {
if (params && params._id) {
const dept = Department.findOne({ _id: params._id }) || Department.findOne({ name: params._id });
if (dept) {
}
}
});
params will be passed into the url. So, initially we won't have the department data and the findOne method will return null, and then later on, when data arrives, we can find the department object.
But if user enters an invalid id, we need to return them 404. Using tracker autorun, how can I distinguish between 2 cases:
a. Data is not there yet, so findOne returns null
b. There is no such data, even in server's mongodb, so findOne will also returns null.
For case a, tracker autorun will work fine, but for case b, I need to know to return 404
I would suggest you to subscribe to data inside template, like below so you know when subscriptions are ready, then you can check data exists or not
Template.myTemplate.onCreated(function onCreated() {
const self = this;
const id = FlowRouter.getParam('_id');
self.subscribe('department', id);
});
Template.myTemplate.onRendered(function onRendered() {
const self = this;
// this will run after subscribe completes sending records to client
if (self.subscriptionsReady()) {
const id = FlowRouter.getParam('_id');
const dept = Department.findOne({ _id: params._id }) || Department.findOne({ name: params._id });
if (dept) {
// found data in db
} else {
// 404 - no department found in db
}
}
});
If you are using Iron-Router, you may try this hack.
Router.route('/stores', function() {
this.render('stores', {});
}, {
waitOn: function() {
return [
Meteor.subscribe('stores_db')
];
}
});
The sample code above will wait for the subscription "stores_db" to complete, before rendering anyhing. Then you can use your findOne logic no problems, ensuring that all documents are availble. This suits your situation.
This is what I used to do before I completely understand MeteorJS publications and subscriptions. I do not recommend my solution, it is very bad to user experience. Users will see the page loading forever while the documents are being download. #Sasikanth gave the correct implementation.
Ok this is what I've got.
The collection called Posts has content and I want to publish this under the name Merchs, the find() in the publish-function finds data but that is not shared to the client where Merchs is always empty.
//shared
Merchs = new Meteor.Collection('merchs');
// Posts has data I want to publish as "Merchs"
this.Posts = new Meteor.Collection('posts');
//server
Merchs.allow({
insert: function(userId, doc) {
return true;
},
update: function(userId, doc, fields, modifier) {
return true;
},
remove: function(userId, doc) {
return true;
}
});
Meteor.publish('merchs', function(data) {
return Posts.find();
});
//client
Deps.autorun( function() {
Session.get('selectedCategories');
subs.subscribe('merchs');
});
When creating your collection, the name in parentheses should be the name of the Mongo collection.
Merchs = new Meteor.Collection('merchs');
Should be:
Merchs = new Mongo.Collection('Posts');
That is, unless you already have a Posts variable defined in code that you didn't show. If you've already defined Posts and you're just looking to make another subscription to the same collection then you don't need this line at all:
Merchs = new Meteor.Collection('merchs');
You also don't need your allow() method (you can just use the one defined for Posts). All you need is the publish() method that you defined.
On the client side you also need:
Meteor.subscribe('merchs');
Also note the use of Mongo.Collection instead of Meteor.Collection which was renamed in Meteor 0.9.1.
You might want to read this excellent answer regarding publish/subscribe: https://stackoverflow.com/a/21853298/4665459
I have the following in my initialize file to get the values loaded in the database on startup:
Meteor.startup(function() {
if(typeof Person.findOne() === 'undefined') {
Person.insert({
name: "",
gender: ["male", "female", "prefer not to say"],
age: 0
});
}
});
And then in the server/abc.js I have:
Meteor.methods({
checkPerson: function (input) {
for (var key in Person) {
if (input === key) {
...
}
}
}
});
This meteor method checkPerson is called in the client side with a string value being passed as its only argument(input).
I want to check this 'input' string value against the name of the key in the Person Collection.
Person has a key called 'gender'. So for instance, if the 'input' holds the string value 'gender' then the if statement should be true but in my case it comes as false and hence the code inside the if statement is never executed.
Any help/guidance with this will be appreciated.
UPDATE
I searched on mongodb documentation and found here: http://docs.mongodb.org/manual/reference/operator/query/exists/ and also using some help from this thread: (using $exists in Mongo with dynamic key names and the native driver for node)
that I could do something like this:
var checkThis = {};
checkThis[input] = { $exists : true };
var p = Person.findOne(checkThis);
So if it finds one then 'p' holds the record or else it will be undefined. But still the above code does not work.
If I were to put directly:
var p = Person.find({gender: {$exists: true} });
then it works.
So I need assistance in getting the code to work with the variable 'input'.
Mongo is a schemaless database - you can insert any document structure you like into a collection and the data store won't complain. Therefore Person won't be able to indicate which fields conform to the pattern.
The most common way people deal with this problem is to use a package which provides a schema layer on top of mongo. With meteor, a popular choice is SimpleSchema, and its related package AutoForm. SimpleSchema allows you to define which fields should be allowed into a collection, and AutoForm gives you a set of helpers to enforce them in your UI.
If, instead, you prefer not to use a package you could do something like the following:
person.js
var REQUIRED_FIELDS = {
name: String,
gender: ['male', 'female', 'prefer not to say'],
age: Number
};
Person = new Meteor.Collection('person');
Person.isValid = function(person) {
try {
check(person, REQUIRED_FIELDS);
return true;
} catch (_error) {
return false;
}
};
Meteor.methods({
'person.insert': function(person) {
check(person, REQUIRED_FIELDS);
return Person.insert(person);
}
});
my-template.js
Template.myTemplate.events({
submit: function() {
var person = {
name: $('#name').val(),
gender: $('#gender').val(),
age: parseInt($('#age').val(), 10)
};
if (Person.isValid(person))
Meteor.call('person.insert', person);
else
alert('invalid person');
}
});
Here we are using meteor's check package to do some basic field validation. By adding an isValid helper to the Person collection, we can validate the schema without the need for a method call. Best of all we can reuse the same check when inserting a new document.
I just got done with the rough draft of my app, and thought it was time to remove autopublish and insecure mode. I started transfering all the stray update and insert methods I had been calling on the client to methods. But now I'm having trouble returning a username from an ID.
My function before: (that worked, until I removed autopublish)
challenger: function() {
var postId = Session.get('activePost');
var post = Posts.findOne(postId);
if (post.challenger !== null) {
var challenger = Meteor.users.findOne(post.challenger);
return challenger.username;
}
return false;
}
Now what I'm trying:
Template.lobby.helpers({
challenger: function() {
var postId = Session.get('activePost');
var post = Posts.findOne(postId);
if (post.challenger !== null) {
var userId = post.challenger;
Meteor.call('getUsername', userId, function (err, result) {
if (err) {
console.log(err);
}
return result;
});
}
return false;
},
Using:
Meteor.methods({
getUsername: function(userId) {
var user = Meteor.users.findOne({_id: userId});
var username = user.username;
return username;
},
...
})
I have tried blocking the code, returning values only once they're defined, and console.logging in the call-callback (which returned the correct username to console, but the view remained unchanged)
Hoping someone can find the obvious mistake I'm making, because I've tried for 3 hours now and I can't figure out why the value would be returned in console but not returned to the template.
Helpers need to run synchronously and should not have any side effects. Instead of calling a method to retrieve the user, you should ensure the user(s) you need for that route/template are published. For example your router could wait on subscriptions for both the active post and the post's challenger. Once the client has the necessary documents, you can revert to your original code.
I have a question on meteor's parties example.
If I call this code:
Parties.allow({
insert: function () {
return true;
},
remove: function (){
return true;
},
update: function() {
return true;
}
});
everybody can do insert, remove and update.
The code from the example is
Parties.allow({
insert: function (userId, party) {
return false; // no cowboy inserts -- use createPage method
},
update: function (userId, parties, fields, modifier) {
return _.all(parties, function (party) {
if (userId !== party.owner)
return false; // not the owner
var allowed = ["title", "description", "x", "y"];
if (_.difference(fields, allowed).length)
return false; // tried to write to forbidden field
// A good improvement would be to validate the type of the new
// value of the field (and if a string, the length.) In the
// future Meteor will have a schema system to makes that easier.
return true;
});
},
remove: function (userId, parties) {
return ! _.any(parties, function (party) {
// deny if not the owner, or if other people are going
return party.owner !== userId || attending(party) > 0;
});
}
});
So my question is where the variables useriD and party at this line for example
insert: function (userId, party) {
are defined?
Are these the variables I call in the method
Meteor.call("createParty", variable1, variable2)
? But this wouldn't make sense because the client calls
Meteor.call('createParty', {
title: title,
description: description,
x: coords.x,
y: coords.y,
public: public
}
I hope somebody can explain the allow functions to me? Thanks!
To understand allow/deny, you need to understand where the userId and doc parameters come from. (Just as in any function definition, the actual parameter names don't matter.) Looking just at the Parties insert example:
Parties.allow({
insert: function (userId, party) {
return false; // no cowboy inserts -- use createPage method
}
});
The party parameter is the doc that's being inserted:
Parties.insert(doc);
The userId parameter is set automatically IF you're using the Meteor Accounts auth system. Otherwise, you have to set it yourself on the server. How do you do that?
In general, you call code on the server from the client by using Meteor.call(). Since there's no built-in API to set userId (other than Accounts), you have to write your own (goes in your server code):
Meteor.methods({
setUserId: function(userId) {
this.setUserId(userId);
}
});
Then you can call it like this, anywhere in your client code:
Meteor.call('setUserId', userId);
1) Where the variables useriD and party are defined? Nowhere! the intention is that no user can call this function.
This is in order to proctect the database from users that could insert manually new parties using the console. Remmember that Meteor replicates the database in client and server.
Any user could insert manually new parties through the console. This is fine. But then the server would reject the insert since it is not allowed.
2) Are these the variables I call in the method Meteor.call("createParty", variable1, variable2)? Yes the variables are available, but this code is not using the correct definition which is:
Meteor.methods({
createParty: function (options) {
And afterwards it is used as
Meteor.call('createParty',
{ title: title, public: public, ... }, // options array!!
function (error, party) { ... } // function executed after the call
);
Did it help you?