Chapter 8. Security with the Allow and Deny Rules
In the previous chapter, we created our admin user and prepared the editPost
template. In this chapter, we will make this template work so that we can create and edit posts using it.
To make it possible to insert and update documents in our database, we need to set constraints so that not everybody can change our database. In Meteor, this is done using the allow and deny rules. These functions will check documents before they are inserted into the database.
In this chapter, you will cover the following topics:
- Adding and updating posts
- Using the allow and deny rules to control the updating of the database
- Using methods on the server for more flexibility
- Using method stubs to enhance user experience
Note
If you've jumped right into the chapter and want to follow the examples, download the previous chapter's code examples from either the book's web page at https://www.packtpub.com/books/content/support/17713 or from the GitHub repository at https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter7.
These code examples will also contain all the style files, so we don't have to worry about adding CSS code along the way.
Adding a function to generate slugs
In order to generate slugs from our post's titles, we will use the underscore-string
library, which comes with a simple slugify()
function. Luckily, a wrapper package for this library already exists on the Meteor package servers. To add it, we run the following command from the terminal in our my-meteor-blog
folder:
$ meteor add wizonesolutions:underscore-string
This will extend underscore
, which is used by default in Meteor, with extra string functions such as _.slugify()
, to generate a slug from strings.
Creating a new post
Now that we can generate slugs for each created page, we can proceed to add the saving process to the editPost
template.
To do so, we need to create a JavaScript file for our editPost
template by saving a file called editPost.js
to the my-meteor-blog/client/templates
folder. Inside this file, we will add an event for the Save button of the template:
Template.editPost.events({ 'submit form': function(e, template){ e.preventDefault(); console.log('Post saved'); } });
Now, if we go to the /create-post
route and click on the Save Post button, the Post saved log should appear in the browser's console.
Saving a post
In order to save the post, we will simply take the form's content and store it in the database. Later, we'll redirect to the newly created post page. To do so, we extend our click event with the following lines of code:
Template.editPost.events({ 'submit form': function(e, tmpl){ e.preventDefault(); var form = e.target, user = Meteor.user();
We get the current user so that we can add him later as the post's author. We then generate a slug from the post's title using our slugify()
function:
var slug = _.slugify(form.title.value);
Following this, we insert the post document into the Posts
collection using all other form fields. For the timeCreated
property, we get the current Unix timestamp using the moment
package, which we added in Chapter 1, Getting Started with Meteor.
The owner
field will later help us to determine by which user this post was created:
Posts.insert({ title: form.title.value, slug: slug, description: form.description.value, text: form.text.value, timeCreated: moment().unix(), author: user.profile.name, owner: user._id }, function(error) { if(error) { // display the error to the user alert(error.reason); } else { // Redirect to the post Router.go('Post', {slug: slug}); } });
The second argument we pass to the insert()
function is a callback function provided by Meteor that will receive an error argument if something goes wrong. If an error happens, we alert it, and if everything goes fine, we redirect to the newly inserted post using our generated slug.
Since our route controller will then subscribe to a post with this slug, it will be able to load our newly created post and display it in the post template.
Now, if we go to the browser, fill in the form, and click on the Save button, we should have created our first own post!
Saving a post
In order to save the post, we will simply take the form's content and store it in the database. Later, we'll redirect to the newly created post page. To do so, we extend our click event with the following lines of code:
Template.editPost.events({ 'submit form': function(e, tmpl){ e.preventDefault(); var form = e.target, user = Meteor.user();
We get the current user so that we can add him later as the post's author. We then generate a slug from the post's title using our slugify()
function:
var slug = _.slugify(form.title.value);
Following this, we insert the post document into the Posts
collection using all other form fields. For the timeCreated
property, we get the current Unix timestamp using the moment
package, which we added in Chapter 1, Getting Started with Meteor.
The owner
field will later help us to determine by which user this post was created:
Posts.insert({ title: form.title.value, slug: slug, description: form.description.value, text: form.text.value, timeCreated: moment().unix(), author: user.profile.name, owner: user._id }, function(error) { if(error) { // display the error to the user alert(error.reason); } else { // Redirect to the post Router.go('Post', {slug: slug}); } });
The second argument we pass to the insert()
function is a callback function provided by Meteor that will receive an error argument if something goes wrong. If an error happens, we alert it, and if everything goes fine, we redirect to the newly inserted post using our generated slug.
Since our route controller will then subscribe to a post with this slug, it will be able to load our newly created post and display it in the post template.
Now, if we go to the browser, fill in the form, and click on the Save button, we should have created our first own post!
Editing posts
So saving works. What about editing?
When we click on the Edit button in the post, we will be shown the editPost
template again. This time, however, the form fields are filled with the data from the post. So far so good, but if we press the Save button now, we will create another post instead of updating the current one.
Updating the current post
Since we set the data context of the editPost
template, we can simply use the presence of the post's _id
field as an indicator to update, instead of inserting the post data:
Template.editPost.events({ 'submit form': function(e, tmpl){ e.preventDefault(); var form = e.target, user = Meteor.user(), _this = this; // we need this to reference the slug in the callback // Edit the post if(this._id) { Posts.update(this._id, {$set: { title: form.title.value, description: form.description.value, text: form.text.value }}, function(error) { if(error) { // display the error to the user alert(error.reason); } else { // Redirect to the post Router.go('Post', {slug: _this.slug}); } }); // SAVE } else { // The insertion process ... } } });
Knowing the _id
, we can simply update the current document using the $set
property. Using $set
will only overwrite the title
, description
, and text
fields. The other fields will be left as they are.
Note that we now also need to create the _this
variable on top of the function in order to access the slug
property of the current data context in the callback later. This way, we can later redirect to our edited post page.
Now, if we save the file and go back to our browser, we can edit the post and click on Save, and all changes will be saved as expected to our database.
Now, we can create and edit posts. In the next section, we will learn how to restrict updates to the database by adding the allow and deny rules.
Updating the current post
Since we set the data context of the editPost
template, we can simply use the presence of the post's _id
field as an indicator to update, instead of inserting the post data:
Template.editPost.events({ 'submit form': function(e, tmpl){ e.preventDefault(); var form = e.target, user = Meteor.user(), _this = this; // we need this to reference the slug in the callback // Edit the post if(this._id) { Posts.update(this._id, {$set: { title: form.title.value, description: form.description.value, text: form.text.value }}, function(error) { if(error) { // display the error to the user alert(error.reason); } else { // Redirect to the post Router.go('Post', {slug: _this.slug}); } }); // SAVE } else { // The insertion process ... } } });
Knowing the _id
, we can simply update the current document using the $set
property. Using $set
will only overwrite the title
, description
, and text
fields. The other fields will be left as they are.
Note that we now also need to create the _this
variable on top of the function in order to access the slug
property of the current data context in the callback later. This way, we can later redirect to our edited post page.
Now, if we save the file and go back to our browser, we can edit the post and click on Save, and all changes will be saved as expected to our database.
Now, we can create and edit posts. In the next section, we will learn how to restrict updates to the database by adding the allow and deny rules.
Restricting database updates
Until now, we simply added the insert and update functionality to our editPost
template. However, anybody can insert and update data if they just type an insert
statement into their browser's console.
To prevent this, we need to properly check for insertion and update rights on the server side before updating the database.
Meteor's collections come with the allow and deny functions, which will be run before every insertion or update to determine whether the action is allowed or not.
The allow rules let us allow certain documents or fields to be updated, whereas the deny rules overwrite any allow rules and definitely deny any action on its collection.
To make this more visible, let's visualize an example where we define two allow rules; one will allow certain documents' title
fields to be changed and another will allow only editing of the description
fields, but an additional deny rule can prevent one specific document to be edited in any case.
Removing the insecure package
To start using the allow and deny rules, we need to remove the insecure
package from our app so that no client can simply make changes to our database without passing our allow and deny rules.
Quit the running meteor
instance using Ctrl + C in the terminal and run the following command:
$ meteor remove insecure
After we have successfully removed the package, we can run Meteor again using the meteor
command.
When we now go to our browser and try to edit any post, we will see an alert window stating Access denied. Remember that we added this alert()
call before, when an update or insert action failed?
Adding our first allow rules
In order to make our posts editable again, we need to add allow rules to enable database updates again.
To do so, we will add the following allow rules to our my-meteor-blog/collections.js
file, but in this case we'll execute them only on the server side by checking against Meteor's isServer
variable, as follows:
if(Meteor.isServer) { Posts.allow({ insert: function (userId, doc) { // The user must be logged in, and the document must be owned by the user return userId && doc.owner === userId && Meteor.user().roles.admin; },
In the insertion allow rule , we will insert the document only if the post owner matches the current user and if the user is an admin, which we can determine by the roles.admin
property we added in the previous chapter.
If the allow rule returns false
, the insertion of the document will be denied. Otherwise, we will successfully add a new post. Updating works the same way, just that we only check whether the current user is an admin:
update: function (userId, doc, fields, modifier) { // User must be an admin return Meteor.user().roles.admin; }, // make sure we only get this field from the documents fetch: ['owner'] }); }
The arguments passed to the update
function are listed in the following table:
Field |
Description |
---|---|
|
The user ID of the current logged-in user, who performs that |
|
The document from the database, without the proposed changes |
|
An array with field parameters that will be updated |
|
The modifier the user passed to the |
The
fetch
property, which we specify last in the allow rule's object, determines which fields of the current document should be passed to the update rule. In our case, we only need the owner
property for our update rule. The fetch
property exists for performance reasons, to prevent unnecessarily large documents from being passed to the rule's functions.
Note
Additionally, we can specify the remove()
rule and the transform()
function. The
remove()
rule will get the same arguments as the insert()
rule and allow or prevent removal of documents.
The transform()
function can be used to transform the document before being passed to the allow or deny rules, for example, to normalize it. However, be aware that this won't change the document that gets inserted into the database.
If we now try to edit a post in our website, we should be able to edit all posts as well as create new ones.
Removing the insecure package
To start using the allow and deny rules, we need to remove the insecure
package from our app so that no client can simply make changes to our database without passing our allow and deny rules.
Quit the running meteor
instance using Ctrl + C in the terminal and run the following command:
$ meteor remove insecure
After we have successfully removed the package, we can run Meteor again using the meteor
command.
When we now go to our browser and try to edit any post, we will see an alert window stating Access denied. Remember that we added this alert()
call before, when an update or insert action failed?
Adding our first allow rules
In order to make our posts editable again, we need to add allow rules to enable database updates again.
To do so, we will add the following allow rules to our my-meteor-blog/collections.js
file, but in this case we'll execute them only on the server side by checking against Meteor's isServer
variable, as follows:
if(Meteor.isServer) { Posts.allow({ insert: function (userId, doc) { // The user must be logged in, and the document must be owned by the user return userId && doc.owner === userId && Meteor.user().roles.admin; },
In the insertion allow rule , we will insert the document only if the post owner matches the current user and if the user is an admin, which we can determine by the roles.admin
property we added in the previous chapter.
If the allow rule returns false
, the insertion of the document will be denied. Otherwise, we will successfully add a new post. Updating works the same way, just that we only check whether the current user is an admin:
update: function (userId, doc, fields, modifier) { // User must be an admin return Meteor.user().roles.admin; }, // make sure we only get this field from the documents fetch: ['owner'] }); }
The arguments passed to the update
function are listed in the following table:
Field |
Description |
---|---|
|
The user ID of the current logged-in user, who performs that |
|
The document from the database, without the proposed changes |
|
An array with field parameters that will be updated |
|
The modifier the user passed to the |
The
fetch
property, which we specify last in the allow rule's object, determines which fields of the current document should be passed to the update rule. In our case, we only need the owner
property for our update rule. The fetch
property exists for performance reasons, to prevent unnecessarily large documents from being passed to the rule's functions.
Note
Additionally, we can specify the remove()
rule and the transform()
function. The
remove()
rule will get the same arguments as the insert()
rule and allow or prevent removal of documents.
The transform()
function can be used to transform the document before being passed to the allow or deny rules, for example, to normalize it. However, be aware that this won't change the document that gets inserted into the database.
If we now try to edit a post in our website, we should be able to edit all posts as well as create new ones.
Adding our first allow rules
In order to make our posts editable again, we need to add allow rules to enable database updates again.
To do so, we will add the following allow rules to our my-meteor-blog/collections.js
file, but in this case we'll execute them only on the server side by checking against Meteor's isServer
variable, as follows:
if(Meteor.isServer) { Posts.allow({ insert: function (userId, doc) { // The user must be logged in, and the document must be owned by the user return userId && doc.owner === userId && Meteor.user().roles.admin; },
In the insertion allow rule , we will insert the document only if the post owner matches the current user and if the user is an admin, which we can determine by the roles.admin
property we added in the previous chapter.
If the allow rule returns false
, the insertion of the document will be denied. Otherwise, we will successfully add a new post. Updating works the same way, just that we only check whether the current user is an admin:
update: function (userId, doc, fields, modifier) { // User must be an admin return Meteor.user().roles.admin; }, // make sure we only get this field from the documents fetch: ['owner'] }); }
The arguments passed to the update
function are listed in the following table:
Field |
Description |
---|---|
|
The user ID of the current logged-in user, who performs that |
|
The document from the database, without the proposed changes |
|
An array with field parameters that will be updated |
|
The modifier the user passed to the |
The
fetch
property, which we specify last in the allow rule's object, determines which fields of the current document should be passed to the update rule. In our case, we only need the owner
property for our update rule. The fetch
property exists for performance reasons, to prevent unnecessarily large documents from being passed to the rule's functions.
Note
Additionally, we can specify the remove()
rule and the transform()
function. The
remove()
rule will get the same arguments as the insert()
rule and allow or prevent removal of documents.
The transform()
function can be used to transform the document before being passed to the allow or deny rules, for example, to normalize it. However, be aware that this won't change the document that gets inserted into the database.
If we now try to edit a post in our website, we should be able to edit all posts as well as create new ones.
Adding a deny rule
To improve security, we can fix the owner of the post and the time when it was created. We can prevent changes to the owner and the timeCreated
and slug
fields by adding an additional deny rule to our Posts
collection, as follows:
if(Meteor.isServer) { // Allow rules Posts.deny({ update: function (userId, docs, fields, modifier) { // Can't change owners, timeCreated and slug return _.contains(fields, 'owner') || _.contains(fields, 'timeCreated') || _.contains(fields, 'slug'); } }); }
This rule will simply check whether the fields
argument contains one of the restricted fields. If it does, we deny the update to this post. So, even if our previous allow rules have passed, our deny rule ensures that the document doesn't change.
We can try the deny rule by going to our browser's console, and when we are at a post page, typing the following commands:
Posts.update(Posts.findOne()._id, {$set: {'slug':'test'}});
This should give you an error stating update failed: Access denied, as shown in the following screenshot:
Though we can add and update posts now, there is a better way of adding new posts than simply inserting them into our Posts
collection from the client side.
Adding posts using a method call
Methods are functions that can be called from the client and will be executed on the server.
Method stubs and latency compensation
The advantage of methods is that they can execute code on the server, having the full database and a stub method on the client available.
For example, we can have a method do something on the server and simulate the expected outcome in a stub method on the client. This way, the user doesn't have to wait for the server's response. A stub can also invoke an interface change, such as adding a loading indicator.
One native example of a method call is Meteor's Collection.insert()
function, which will execute a client-side function, inserting the document immediately into the local minimongo
database as well as sending a request executing the real insert
method on the server. If the insertion is successful, the client has the document already inserted. If an error occurs, the server will respond and remove the inserted document from the client again.
In Meteor, this concept is called latency compensation, as the interface reacts immediately to the user's response and therefore compensates the latency, while the server's round trip will happen in the background.
Inserting a post using a method call enables us to simply check whether the slug we want to use for the post already exists in another post. Additionally, we can use the server's time for the timeCreated
property to be sure we are not using an incorrect user timestamp.
Changing the button
In our example, we will simply use the method stub functionality to change the text of the Save button to Saving…
while we run the method on the server. To do so, perform the following steps:
- To start, let's first change the Save button's static text with a template helper so that we can change it dynamically. Open up
my-meteor-blog/client/templates/editPost.html
and replace the Save button code with the following code:<button type="submit" class="save">{{saveButtonText}}</button>
- Now open
my-meteor-blog/client/templates/editPost.js
and add the following template helper function at the beginning of the file:Session.setDefault('saveButton', 'Save Post'); Template.editPost.helpers({ saveButtonText: function(){ return Session.get('saveButton'); } });
Here, we return the session variable named
saveButton
, which we set to the default value,Save Post
, earlier.
Changing the session will allow us to change the text of the Save button later while saving the document.
Adding the method
Now that we have a dynamic Save button, let's add the actual method to our app. For this, we will create a new file called methods.js
directly in our my-meteor-blog
folder. This way, its code will be loaded on the server and the client, which is necessary to execute the method on the client as a stub.
Add the following lines of code to add a method:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } } });
This will add a method called insertPost
. Inside this method, the stub functionality is already added by making use of the isSimulation
property, which is made available in the this
object of the function by Meteor.
The this
object also has the following properties:
unblock()
: This is a function that when called will prevent the method from blocking other method callsuserId
: This contains the current user's IDsetUserId()
: This a function to connect the current client with a certain userconnection
: This is the connection on the server through which this method is called
If isSimulation
is set to true
, the method is not run on the server side but as a stub on the client. Inside this condition, we simply set the saveButton
session variable to Saving…
so that the button text will change:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } else {
To complete the method, we will add the server-side code for post insertion:
var user = Meteor.user(); // ensure the user is logged in if (!user) throw new Meteor.Error(401, "You need to login to write a post");
Here, we get the current user to add the author name and owner ID.
We throw an exception with new Meteor.Error
if the user is not logged in. This will stop the execution of the method and return an error message we define.
We also search for a post with the given slug. If we find one, we prepend a random string to the slug to prevent duplicates. This makes sure that every slug is unique, and we can successfully route to our newly created post:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
Before we insert the newly created post, we add timeCreated
using the moment
library and the author
and owner
properties:
// add properties on the serverside postDocument.timeCreated = moment().unix(); postDocument.author = user.profile.name; postDocument.owner = user._id; Posts.insert(postDocument);
After we insert the document, we return the corrected slug, which will then be received in the callback of the method call as the second argument:
// this will be received as the second argument of the method callback return postDocument.slug; } } });
Method stubs and latency compensation
The advantage of methods is that they can execute code on the server, having the full database and a stub method on the client available.
For example, we can have a method do something on the server and simulate the expected outcome in a stub method on the client. This way, the user doesn't have to wait for the server's response. A stub can also invoke an interface change, such as adding a loading indicator.
One native example of a method call is Meteor's Collection.insert()
function, which will execute a client-side function, inserting the document immediately into the local minimongo
database as well as sending a request executing the real insert
method on the server. If the insertion is successful, the client has the document already inserted. If an error occurs, the server will respond and remove the inserted document from the client again.
In Meteor, this concept is called latency compensation, as the interface reacts immediately to the user's response and therefore compensates the latency, while the server's round trip will happen in the background.
Inserting a post using a method call enables us to simply check whether the slug we want to use for the post already exists in another post. Additionally, we can use the server's time for the timeCreated
property to be sure we are not using an incorrect user timestamp.
Changing the button
In our example, we will simply use the method stub functionality to change the text of the Save button to Saving…
while we run the method on the server. To do so, perform the following steps:
- To start, let's first change the Save button's static text with a template helper so that we can change it dynamically. Open up
my-meteor-blog/client/templates/editPost.html
and replace the Save button code with the following code:<button type="submit" class="save">{{saveButtonText}}</button>
- Now open
my-meteor-blog/client/templates/editPost.js
and add the following template helper function at the beginning of the file:Session.setDefault('saveButton', 'Save Post'); Template.editPost.helpers({ saveButtonText: function(){ return Session.get('saveButton'); } });
Here, we return the session variable named
saveButton
, which we set to the default value,Save Post
, earlier.
Changing the session will allow us to change the text of the Save button later while saving the document.
Adding the method
Now that we have a dynamic Save button, let's add the actual method to our app. For this, we will create a new file called methods.js
directly in our my-meteor-blog
folder. This way, its code will be loaded on the server and the client, which is necessary to execute the method on the client as a stub.
Add the following lines of code to add a method:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } } });
This will add a method called insertPost
. Inside this method, the stub functionality is already added by making use of the isSimulation
property, which is made available in the this
object of the function by Meteor.
The this
object also has the following properties:
unblock()
: This is a function that when called will prevent the method from blocking other method callsuserId
: This contains the current user's IDsetUserId()
: This a function to connect the current client with a certain userconnection
: This is the connection on the server through which this method is called
If isSimulation
is set to true
, the method is not run on the server side but as a stub on the client. Inside this condition, we simply set the saveButton
session variable to Saving…
so that the button text will change:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } else {
To complete the method, we will add the server-side code for post insertion:
var user = Meteor.user(); // ensure the user is logged in if (!user) throw new Meteor.Error(401, "You need to login to write a post");
Here, we get the current user to add the author name and owner ID.
We throw an exception with new Meteor.Error
if the user is not logged in. This will stop the execution of the method and return an error message we define.
We also search for a post with the given slug. If we find one, we prepend a random string to the slug to prevent duplicates. This makes sure that every slug is unique, and we can successfully route to our newly created post:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
Before we insert the newly created post, we add timeCreated
using the moment
library and the author
and owner
properties:
// add properties on the serverside postDocument.timeCreated = moment().unix(); postDocument.author = user.profile.name; postDocument.owner = user._id; Posts.insert(postDocument);
After we insert the document, we return the corrected slug, which will then be received in the callback of the method call as the second argument:
// this will be received as the second argument of the method callback return postDocument.slug; } } });
Changing the button
In our example, we will simply use the method stub functionality to change the text of the Save button to Saving…
while we run the method on the server. To do so, perform the following steps:
- To start, let's first change the Save button's static text with a template helper so that we can change it dynamically. Open up
my-meteor-blog/client/templates/editPost.html
and replace the Save button code with the following code:<button type="submit" class="save">{{saveButtonText}}</button>
- Now open
my-meteor-blog/client/templates/editPost.js
and add the following template helper function at the beginning of the file:Session.setDefault('saveButton', 'Save Post'); Template.editPost.helpers({ saveButtonText: function(){ return Session.get('saveButton'); } });
Here, we return the session variable named
saveButton
, which we set to the default value,Save Post
, earlier.
Changing the session will allow us to change the text of the Save button later while saving the document.
Adding the method
Now that we have a dynamic Save button, let's add the actual method to our app. For this, we will create a new file called methods.js
directly in our my-meteor-blog
folder. This way, its code will be loaded on the server and the client, which is necessary to execute the method on the client as a stub.
Add the following lines of code to add a method:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } } });
This will add a method called insertPost
. Inside this method, the stub functionality is already added by making use of the isSimulation
property, which is made available in the this
object of the function by Meteor.
The this
object also has the following properties:
unblock()
: This is a function that when called will prevent the method from blocking other method callsuserId
: This contains the current user's IDsetUserId()
: This a function to connect the current client with a certain userconnection
: This is the connection on the server through which this method is called
If isSimulation
is set to true
, the method is not run on the server side but as a stub on the client. Inside this condition, we simply set the saveButton
session variable to Saving…
so that the button text will change:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } else {
To complete the method, we will add the server-side code for post insertion:
var user = Meteor.user(); // ensure the user is logged in if (!user) throw new Meteor.Error(401, "You need to login to write a post");
Here, we get the current user to add the author name and owner ID.
We throw an exception with new Meteor.Error
if the user is not logged in. This will stop the execution of the method and return an error message we define.
We also search for a post with the given slug. If we find one, we prepend a random string to the slug to prevent duplicates. This makes sure that every slug is unique, and we can successfully route to our newly created post:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
Before we insert the newly created post, we add timeCreated
using the moment
library and the author
and owner
properties:
// add properties on the serverside postDocument.timeCreated = moment().unix(); postDocument.author = user.profile.name; postDocument.owner = user._id; Posts.insert(postDocument);
After we insert the document, we return the corrected slug, which will then be received in the callback of the method call as the second argument:
// this will be received as the second argument of the method callback return postDocument.slug; } } });
Adding the method
Now that we have a dynamic Save button, let's add the actual method to our app. For this, we will create a new file called methods.js
directly in our my-meteor-blog
folder. This way, its code will be loaded on the server and the client, which is necessary to execute the method on the client as a stub.
Add the following lines of code to add a method:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } } });
This will add a method called insertPost
. Inside this method, the stub functionality is already added by making use of the isSimulation
property, which is made available in the this
object of the function by Meteor.
The this
object also has the following properties:
unblock()
: This is a function that when called will prevent the method from blocking other method callsuserId
: This contains the current user's IDsetUserId()
: This a function to connect the current client with a certain userconnection
: This is the connection on the server through which this method is called
If isSimulation
is set to true
, the method is not run on the server side but as a stub on the client. Inside this condition, we simply set the saveButton
session variable to Saving…
so that the button text will change:
Meteor.methods({ insertPost: function(postDocument) { if(this.isSimulation) { Session.set('saveButton', 'Saving...'); } else {
To complete the method, we will add the server-side code for post insertion:
var user = Meteor.user(); // ensure the user is logged in if (!user) throw new Meteor.Error(401, "You need to login to write a post");
Here, we get the current user to add the author name and owner ID.
We throw an exception with new Meteor.Error
if the user is not logged in. This will stop the execution of the method and return an error message we define.
We also search for a post with the given slug. If we find one, we prepend a random string to the slug to prevent duplicates. This makes sure that every slug is unique, and we can successfully route to our newly created post:
if(Posts.findOne({slug: postDocument.slug})) postDocument.slug = postDocument.slug +'-'+ Math.random().toString(36).substring(3);
Before we insert the newly created post, we add timeCreated
using the moment
library and the author
and owner
properties:
// add properties on the serverside postDocument.timeCreated = moment().unix(); postDocument.author = user.profile.name; postDocument.owner = user._id; Posts.insert(postDocument);
After we insert the document, we return the corrected slug, which will then be received in the callback of the method call as the second argument:
// this will be received as the second argument of the method callback return postDocument.slug; } } });
Calling the method
Now that we have created our insertPost
method, we can change the code in the submit event, where we inserted the post earlier in our editPost.js
file, with a call to our method:
var slug = _.slugify(form.title.value); Meteor.call('insertPost', { title: form.title.value slug: slug, description: form.description.value text: form.text.value, }, function(error, slug) { Session.set('saveButton', 'Save Post'); if(error) { return alert(error.reason); } // Here we use the (probably changed) slug from the server side method Router.go('Post', {slug: slug}); });
As we can see in the callback of the method call, we route to the newly created post using the slug
variable we received as the second argument in the callback. This ensures that if the slug
variable is modified on the server side, we use the modified version to route to the post. Additionally, we reset the saveButton
session variable to change the text to Save Post
again.
That's it! Now, we can create a new post and save it using our newly created insertPost
method. However, editing will still be done from the client side using Posts.update()
, as we now have allow and deny rules, which make sure that only allowed data is modified.
Summary
In this chapter, we learned how to allow and deny database updates. We set up our own allow and deny rules and saw how methods can improve security by moving sensitive processes to the server side. We also improved our procedure of creating posts by checking whether the slug already exists and adding a simple progress indicator.
If you want to dig deeper into the allow and deny rules or methods, take a look at the following Meteor documentations:
You can find this chapter's code examples at https://www.packtpub.com/books/content/support/17713 or on GitHub at https://github.com/frozeman/book-building-single-page-web-apps-with-meteor/tree/chapter8.
In the next chapter, we will make our interface real time by constantly updating the post's timestamps.