Extending Meteor.users

André Neves
All About MeteorJS
Published in
8 min readAug 20, 2015

One of the many confusing topics that seem to come up over and over again within the Meteor community is the handling of the Meteor.users collection. As a Meteor Developer it is important that you understand the ins and outs of the Meteor.users collection as it is the entire basis on which your application will rely on for user authentication and services. That being said, there are tons of great packages in Atmosphere to automate the process of handling users' logins and logouts in your applications. Meteor UserAccounts is, in my opinion, the most extensive of them all, offering easy customization and lots of already-made styling for various front-end frameworks such as SemanticUI, Bootstrap, Foundation, and many more.

That being said, because my aim is to extend the Meteor.users collection in my application, I will not be using an automated user-handling package. Rather, I'll be creating a simple custom authentication system, along with some custom user fields such as first name, last name and age. Luckily, there are ways to achieve this functionality without a lot of work. My goal in this post is to explain how to extend the Meteor.users collection, with custom fields to fit the specific needs of your application.

Let's get started

So now let's get to some coding. To start off, we create a brand new meteor app and after changing into the created directory, we start the app.

meteor create user-extend
cd user-extend
meteor

At this point our app is pretty boring, so let's start by adding the necessary packages:

meteor add kadira:flow-router kadira:blaze-layout
meteor add accounts-password
meteor add msavin:mongol

The list above consists of the two packages necessary for routing and rendering templates on the app (FlowRouter and BlazeLayout), accounts-password for allowing for users to login, and lastly the Mongol Debugging tool that helps with seeing data on MongoDB.

Setting up routes

Below are three simple routes that render the home, login and register templates onto our mainLayout template.

FlowRouter.route(‘/’, {
name: ‘home’,
action: function() {
BlazeLayout.render(“mainLayout”, {
content: “home”
});
}
});
FlowRouter.route(‘/login’, {
name: ‘login’,
action: function() {
BlazeLayout.render(“mainLayout”, {
content: “login”
});
}
});
FlowRouter.route(‘/register’, {
name: ‘register’,
action: function() {
BlazeLayout.render(“mainLayout”, {
content: “register”
});
}
});

Setting up HTML templates

We then set up our login and register templates to look like so (HTML for the templates can be found on the GitHub example repository):

login.html and register.html

As you can see, in the login template we have a simple Email and Password form, whilst in the register template we have a form with Email, Password in addition to First Name and Last Name.

User login event handling

We then jump into the JavaScript to authenticate users in our application. Under login.js we have the following code:

Template.login.events({
‘click #login-button’: function(e, t) {
e.preventDefault();
// Getting values from fields on page
var email = $(‘#login-email’).val(),
password = $(‘#login-password’).val();
// Calling the loginWithPassword function on the user
Meteor.loginWithPassword(email, password, function(error) {
if (error) {
// Returning a sweetAlert
return swal({
title: “Email or password incorrect”,
text: “Please try again”,
timer: 1700,
showConfirmButton: false,
type: “error”
});
} else {
FlowRouter.go(‘/’);
}
});
return false;
}
});

The code above is for an event click on the #login-button. It prevents the default action for the form, gathers the data from the input fields on the page (email and password) and then calls the loginWithPassword function on the user. This will check the Meteor.users collection for a user that matches the data. If it doesn't, it will return the alert and tell the user to create an account.

User registration event handling

Now under register.js we have to create the new user object with the data input by the user on the fields. The syntax is quite similar to the login.js.

Template.register.events({
‘click #register-button’: function(e, t) {
e.preventDefault();
// Retrieve the input field values
var email = $(‘#email’).val(),
firstName = $(‘#first-name’).val(),
lastName = $(‘#last-name’).val(),
password = $(‘#password’).val(),
passwordAgain = $(‘#password-again’).val();
// Trim Helper
var trimInput = function(val) {
return val.replace(/^\s*|\s*$/g, “”);
}
var email = trimInput(email);
// Check password is at least 6 chars long
var isValidPassword = function(pwd, pwd2) {
if (pwd === pwd2) {
return pwd.length >= 6 ? true : false;
} else {
return swal({
title: “Passwords don’t match”,
text: “Please try again”,
showConfirmButton: true,
type: “error”
});
}
}
// If validation passes, supply the appropriate fields to the
// Accounts.createUser function.
if (isValidPassword(password, passwordAgain)) {
Accounts.createUser({
email: email,
firstName: firstName,
lastName: lastName,
password: password
}, function(error) {
if (error) {
console.log(“Error: “ + error.reason);
} else {
FlowRouter.go(‘/’);
}
});
}
return false;
}
});

As you can see we follow a similar event handling scheme as the one in login.js. We first prevent the default action of the form, and get all of the input values. We use a helper to trim the email address and a simple equality function that checks that both passwords are equal and at least 6 characters long. Last but not least, we pass in the new data to the Accounts.createUser method. If there's no error, the user is redirected to the homepage.

All done?

Seems pretty straight forward no? There's only one problem, by default, the user object created by the accounts-password/accounts-base packages only has email, password and username (which we aren't dealing with in this example, thus it is omitted from the user object) fields.

User object view from Mongol Inspector

Even if we were to input data for first and last names on the registration page as in the image to the left, we see that the created user object only has the email field filled out with the correct data (_id is a unique random string automatically created by MongoDB and the password field is omitted for security purposes).

Enter onCreateUser()

The accounts-base package in Meteor has some very handy functions that can be called on the user object. Accounts.onCreateUser() is the one we are after, given that it allows for the customization of the user creation process.

Accounts.onCreateUser() is called whenever a new user is created. It returns the new user object, or throws an error to abort the creation.

Inside the /server folder of our app, we are going to create a new file called account-creation.js. The file will contain the following code:

Accounts.onCreateUser(function(options, user) {
// Use provided profile in options, or create an empty object
user.profile = options.profile || {};
// Assigns first and last names to the newly created user object
user.profile.firstName = options.firstName;
user.profile.lastName = options.lastName;
// Returns the user object
return user;
});

The onCreateUser() function is passed with two arguments: an options and a user object. The options object comes from the Accounts.createUser() call that was made on the client (register.js) when the user tries to register to the application. The user argument is created on the server and contains a proposed user object with all the automatically generated fields required for the user to log in. In the account-creation.js file above, we see that we pass in both of the arguments and we have a simple expression that inputs the provided data from the options argument to the user object, or creates a new empty user object.

By doing so, we are able to assign values from the options argument to the user object that will be created in the database. In the current example we are only passing in the firstName and lastName properties from the options argument into the user object.

If we register a new user we will see that the first and last names are now available in the user object.

Profile property of the user object has now been created

Expanding further

We have now achieved the goal for the post: we successfully created a way to input custom fields into the user object that is created on the database when a user registers to the application. But we don't have to stop here.

This method can also be tied into things like User Roles, User Organizations, Profile Pictures and User Preferences.

// Basic Role Setup
user.roles = [“User”];

Adding the code above to the account-creation.js file will add a roles array to the user object containing "User". This is really handy when using alongside the Roles package. In this manner, every user that registers, will fall under the permissions and restrictions created for the role of a 'User'.

// Basic Profile Picture Setup
user.profile.profPicture = Meteor.absoluteUrl() + “img/default/user.jpg”;

The snippet above will add a field in the profile of the user object for the string containing the user's profile picture. We use Meteor.absoluteUrl() to get the root url for the app, and then concatenate the relative address to the default image file.

// Basic Organization Setup
user.profile.organization = [“Org”];

Once again, the code snippet above will add an array with "Org" to the user's profile object. This same format can be applied to any other type of data that you want to have available for each user in your application.

The idea is to use of these newly created fields in the user object to customize the app’s experience for the user. We can perform checks upon the data on the user object and have the app respond accordingly. (e.g. If the user doesn’t belong to organization XYZ, then perform such task, otherwise perform another task).

Publishing correct user data

It is important to note that, although you can add all of these custom fields to the user object, you must also beware of the exact data you are sending to each client. By default, Meteor has the autopublish package installed, which publishes all of the data from MongoDB to all clients. It is because of that we can easily see the custom fields in the user object. To avoid this security risk, we should remove autopublish from our Meteor app and take on publishing the data ourselves.

Meteor.publish(‘userData’, function() {
var currentUser;
currentUser = this.userId;
if (currentUser) {
return Meteor.users.find({
_id: currentUser
}, {
fields: {
// Default
“emails”: 1,
// Created profile property
“profile”: 1,
// Created roles property
“roles”: 1
}
});
} else {
return this.ready();
}
});

Create a new file called userdata-pubs.js on your app's server folder and use the code above. This snippet publishes to all clients their individual user's information and, in this example, the fields getting published are emails, profile and roles. Remember to add fields to this publication if you add more custom fields to your user object. This method allows us to send only the relevant data to the client, being much more performant.

Wrap up

We are all done! I hope this little introduction to extending the Meteor.users collection was helpful for you and your application development. For more information regarding Meteor and Accounts-specific methods and functions such as onCreateUser(), refer to the official Meteor Docs.

Feel free to comment with questions, suggestions and any other ideas you have about this topic.

Cheers,

— AN

Github Repo: https://github.com/andrerfneves/meteor-users-extend

--

--