Permissions
Now that we can sign up to our site very easily it's time added permission to our users to do things.
Collections are cautious
Upon removing the insecure
package our collections have now become very cautious about who can run insert()
, update()
or remove()
. They treat everyone with mistrust, and have a kind of whitelist and blacklist that they consult everytime someone tries to do something.
Basically they check that you are on the whitelist, and not on the blacklist.
Our "whitelist" is the allow()
function, which works like this (example stolen from Meteor docs):
Example of allow()
Posts = new Mongo.Collection("posts");
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);
},
update: function (userId, doc, fields, modifier) {
// can only change your own documents
return doc.owner === userId;
},
remove: function (userId, doc) {
// can only remove your own documents
return doc.owner === userId;
},
fetch: ['owner']
});
Remember that "document" is noSQL terminology for a "row" in the database.
As you can see we provide a function for the keys insert
, update
, and remove
. Inside each function we can do whatever we want with the given parameters, so long as we return either true or false - if the function returns true, permission is granted. So if we were to insert a new Post
, the insert
function would be run beforehand, making sure you are allowed to do it.
Note that userId
is the _id
of the currently logged in user. If no user is logged in then this will be undefined
.
You may have also noticed fetch: ['owner']
at the bottom. Again from the Meteor docs:
"When calling update or remove Meteor will by default fetch the entire document doc from the database. If you have large documents you may wish to fetch only the fields that are actually used by your functions. Accomplish this by setting fetch to an array of field names to retrieve."
So since in the update()
and remove()
functions we only use one attribute of doc
(doc.owner
), we don't bother fetching all the post's attributes - just the owner
attribute.
Team permissions
Let's start with our teams. The Posts
example from the docs is perfect for our usecase - so let's just copy that in (but we'll call the foreign key ownerId
instead of owner
for clarity):
both/collections/teams.js
Teams = new Mongo.Collection('teams');
Teams.allow({
insert: function (userId, doc) {
return (userId && doc.ownerId === userId);
},
update: function (userId, doc, fields, modifier) {
return doc.ownerId === userId;
},
remove: function (userId, doc) {
return doc.ownerId === userId;
},
fetch: ['ownerId']
});
We can test that this works by using the browser console:
Modifying our teams to work with users
Obviously we have an issue here - our teams currently don't have the 'ownerId' attribute. To fix this we need to do three things:
- Update our seed data
- Update the team
insert()
call to include the current user ID as theownerId
- Scope the
teams
publication to only return teams owned by the logged in user
1. Updating our seed data for accounts
Now that our teams belong to users, we need to update our seed data to reflect this. We'll make it so our seed data creates a new dummy user, and assign our teams to this user:
server/seeds.js
Meteor.startup(function () {
var dummyUserEmail = '[email protected]'
if (Meteor.users.find({"emails.address": dummyUserEmail}).count() == 0){
// Create a test user. `createUser` returns the id of the created user
var ownerId = Accounts.createUser({
email: dummyUserEmail,
password: 'matthew'
});
[
{
name: "Barcelona",
gameIds: [],
ownerId: ownerId
},
{
name: "Real Madrid",
gameIds: [],
ownerId: ownerId
},
{
name: "Matt's team",
gameIds: [],
ownerId: ownerId
}
].forEach(function(team){
Teams.insert(team);
});
// Create a game
var team1 = Teams.find().fetch()[0];
var team2 = Teams.find().fetch()[1];
var game = {
completed: false,
createdAt: new Date,
teams: [
{name: team1.name, _id: team1._id, score: 0},
{name: team2.name, _id: team2._id, score: 0}
]
};
gameId = Games.insert(game);
// Add this game to both teams gameIds
Teams.update({_id: team1._id}, {$addToSet: { gameIds: gameId}});
Teams.update({_id: team2._id}, {$addToSet: { gameIds: gameId}});
}
});
Here we introduce a couple of new things. Firstly let's have a look at this piece of code:
if (Meteor.users.find({"emails.address": dummyUserEmail}).count() == 0){
Since we don't want to run our code if our dummy user already exists, we check for any users with a registered email of dummyUserEmail
. It may look odd because we use dot notation in our query ("emails.address"). The reason being our data structure looks like this:
emails: [
// each email address can only belong to one user.
{ address: "[email protected]", verified: true },
{ address: "[email protected]", verified: false }
],
Using "emails.address" allows us to search through every email address (docs).
Secondly we use Accounts.createUser()
, which is provided by the accounts-password
package (docs) and allows us to easily create a new account.
Now if we reset our app once again:
meteor reset
Our teams should each now have an ownerId
.
2. Updating insert()
for teams
Feel free to try this one yourself. Your task is to make sure that whenever a new team is created, it has the current user's ID as the ownerID.
Here is what I came up with:
client/views/teams.js
...
'submit form.create-team': function(e, tpl){
e.preventDefault();
var team = {
name: tpl.$('input[name=name]').val(),
ownerId: Meteor.userId()
};
Teams.insert(team, function(error, _id){
if(error){
alert(error);
Session.set('isCreatingTeam', true);
Tracker.afterFlush(function(){
tpl.$('input[name=name]').val(team.name);
});
}
});
Session.set('isCreatingTeam', false);
}
...
3. Scope the teams publication
Finally we need to only show teams that belong to the logged in user. Before we start doing this, we should add a team that has no ownerId
, so we can tell if our changes are successful. We can do this quite easily using Mongol:
Now there are two places we could make this happen:
Firstly, we could do it in our teams
helper:
client/views/teams.js
Template.teams.helpers({
isCreatingTeam: function(){
return Session.get('isCreatingTeam');
},
teams: function(){
return Teams.find({ownerId: Meteor.userId()});
}
});
...
Or secondly we could do it in our publications:
server/publications.js
Meteor.publish('teams', function(){
return Teams.find({ownerId: this.userId});
});
Meteor.publish('games', function(){
return Games.find();
});
Note that instead of Meteor.userId()
we have to use this.userId
in publications.
Which would you choose - helper or publication?
You could think of it like damming a river, with the river being data. We could dam it on the server and only let a certain amount into the client (residing in Minimongo). Or we could dam it in the client and only let a certain amount into the DOM.
Naturally we don't want other users's teams floating around in Minimongo as it will make the app slower and any user would be able to access other user's teams in the browser console. So we'll dam it in the server:
server/publications.js
Meteor.publish('teams', function(){
return Teams.find({ownerId: this.userId});
});
Meteor.publish('games', function(){
return Games.find();
});
And now our extra team with no ownerId
disappears. We'd better delete it since we won't be seeing it anymore. But how will we do this?
You'll notice it's no longer in Mongol, as Mongol only reveals Minimongo data. And since we now can only remove teams we own (remember the allow()
rules), we can't remove it in the browser either. Therefore our only option is to do it in Mongo itself. See if you can work out how to do it using the commands below (you will have to fill in the empty strings):
meteor mongo
db.teams.find({name: ""})
db.teams.remove({_id: ""})
Modifying our games to work with users
With teams working correctly we now need to do the same with games. Since I've explained everything I'll just present the code I used (again feel free to try yourself before reading mine).
both/collections/games.js
Games = new Mongo.Collection('games');
Games.allow({
insert: function (userId, doc) {
return (userId && doc.ownerId === userId);
},
update: function (userId, doc, fields, modifier) {
return doc.ownerId === userId;
},
remove: function (userId, doc) {
return doc.ownerId === userId;
},
fetch: ['ownerId']
});
server/seeds.js
...
// Create a game
var team1 = Teams.find().fetch()[0];
var team2 = Teams.find().fetch()[1];
var game = {
completed: false,
createdAt: new Date,
ownerId: ownerId,
teams: [
{name: team1.name, _id: team1._id, score: 0},
{name: team2.name, _id: team2._id, score: 0}
]
};
gameId = Games.insert(game);
...
meteor reset
client/views/games.js
...
"submit form.form-create": function(e, tpl){
e.preventDefault();
var team1 = {
id: tpl.$("select[name='teamOne']").val(),
name: tpl.$("select[name='teamOne'] option:selected").text(),
score: 0
};
var team2 = {
id: tpl.$("select[name='teamTwo']").val(),
name: tpl.$("select[name='teamTwo'] option:selected").text(),
score: 0
};
var game = {
created_at: new Date(),
ownerId: Meteor.userId(),
teams: [team1, team2],
completed: false
};
var gameId = Games.insert(game);
// Add this game to both teams gameIds
Teams.update({_id: team1.id}, {$addToSet: { gameIds: gameId}});
Teams.update({_id: team2.id}, {$addToSet: { gameIds: gameId}});
Session.set('isCreatingGame', false);
}
...
server/publications.js
Meteor.publish('teams', function(){
return Teams.find({ownerId: this.userId});
});
Meteor.publish('games', function(){
return Games.find({ownerId: this.userId});
});
With that done we now have our user account up and running. However there is still an issue we need to address: the createdAt
date of the games could be wrong if the user's time settings are incorrect. To fix this, we can create a server function (called a "method") that would set the time and insert the game. In the next chapter we will learn how to create methods and routes.