15

I'm trying to use the 'roles' package available on Atmosphere but I can't get it to work with Accounts.onCreateUser(), I can get the example on github. When I register a user, I want to add a role to them, when I test whether the role is assigned, it's not picking it up.

Here's my code

/server/users.js

Accounts.onCreateUser(function(options, user){
  var role = ['admin'];
  Roles.addUsersToRoles(user, role);
  return user;
});

/client/page.js

Template.hello.events({
  'click input': function () {
    var loggedInUser = Meteor.user();
    if (Roles.userIsInRole(loggedInUser, ['admin'])) {
      console.log("Hi Admin!");
    }else{
      console.log("Please Log In");
    }
  }
});
user1627990
  • 2,587
  • 2
  • 16
  • 18

7 Answers7

13

If you look at the code being used in the Roles package you will see that they use your passed in user/userId to perform a query on the user's collection (here, starting at line ~623):

try {
  if (Meteor.isClient) {
    // On client, iterate over each user to fulfill Meteor's
    // 'one update per ID' policy
    _.each(users, function (user) {
      Meteor.users.update({_id: user}, update)
    })
  } else {
    // On the server we can use MongoDB's $in operator for
    // better performance
    Meteor.users.update(
      {_id: {$in: users}},
      update,
      {multi: true})
  }
}

Since onCreateUser is called before the user object is inserted into the collection (docs: The returned document is inserted directly into the Meteor.users collection), Roles cannot find it when it performs the query.

In order to fix this you must wait until the user is inserted into the collection. If you look at the Roles package all of their examples show this. Like here, (second example, comments added), along with many others:

// insert user and retrieve the id
id = Accounts.createUser({
  email: user.email,
  password: "apple1",
  profile: { name: user.name }
});

// now we can verify that the user was inserted and add permissions
if (user.roles.length > 0) {
  Roles.addUsersToRoles(id, user.roles);
}

Hope that shines some light on your issue. So basically just insert the user and then add the permissions after.

Firo
  • 15,448
  • 3
  • 54
  • 74
  • Excellent answer. I've seen this question come up before and am open to suggestions. Perhaps just calling this out specifically in the docs? – alanning Mar 26 '14 at 12:35
  • @alanning thanks! Yeah that would probably catch some of the confusion. I would personally recommend adding something to the docs. Even just adding a note after the *client* section on the atmosphere page would probably catch some of these cases. Otherwise, hopefully, people will start finding the SO posts related to it. – Firo Mar 26 '14 at 13:45
  • 3
    What about users created using accounts-ui? – Islam A. Hassan Dec 04 '14 at 13:17
  • @iah.vector, I honestly cannot say for certain (do not do much Meteor dev currently), but unless they made some changes to Meteor since this answer, it would be the same. Accounts-UI is (was) just leveraging Meteor's built in accounts creation, it is just a visual wrapper. Nothing super fancy. But again, if this is of concern to you, you would need to run some testing. It would make sense to me that they would change how `onCreateUser` is implemented (Meteor, not Accounts-UI) so it may work as expected now. – Firo Dec 04 '14 at 14:51
  • Wouldn't it be better to put the role assignment part into the callback for Accounts.createUser? – hiester May 01 '15 at 14:25
  • @hiester - The OP is creating the user on the server and the createUser callback is client only [(docs)](http://docs.meteor.com/#/full/accounts_createuser). – Firo May 01 '15 at 15:26
  • @iah.vector @heister @Firo Take a look at my solution below. Clearly you want to use `onCreateUser`, the below will let you do that. This answer I don't think actually provides a solution. – DoctorPangloss May 16 '15 at 21:17
  • @alanning this answer is linked to in your docs, but I just wanted to ask if there is any reason to use this over DoctorPangloss's (more recent) answer? Or is that answer also a good way to go? – Dan Jun 21 '15 at 21:23
  • 3
    @Dan, @DoctorPangloss's answer let's you use `onCreateUser` which can be convenient. We use our own custom Meteor method for user creation so in our app we do it the way @Firo suggests. I added an example of a new function that sets the roles directly on the user object [here](https://github.com/alanning/meteor-roles/tree/master/examples/rolesWithAccountsUI) but that will only work as long as the db structure that roles uses stays the same (as DP mentions in one of his comments). – alanning Jun 22 '15 at 03:51
  • 1
    The next version of roles will probably take a flag that lets you indicate that the user roles should be updated directly on the user object so they can be used directly in onCreateUser. No plan on when that will be tho. – alanning Jun 22 '15 at 03:53
  • Are you guys missing Meteor.setTimeout? I [tried it out](http://stackoverflow.com/a/31064952/256272) and it seems to work fine. Is there any pitfalls with this method? – Joe Jun 26 '15 at 04:33
9

To add things to a user document after it has been inserted by the accounts package, try this pattern. Note, you must meteor add random in order to generate a userId, which is safe to do in this context.

Accounts.onCreateUser(function (options, user) {
    // Semantics for adding things to users after the user document has been inserted
    var userId = user._id = Random.id();
    var handle = Meteor.users.find({_id: userId}, {fields: {_id: 1}}).observe({
        added: function () {
            Roles.addUsersToRoles(userId, ['admin']);
            handle.stop();
            handle = null;
        }
    });

    // In case the document is never inserted
    Meteor.setTimeout(function() {
        if (handle) {
            handle.stop();
        }
    }, 30000);

    return user;
});
DoctorPangloss
  • 2,994
  • 1
  • 18
  • 22
  • What is the advantage of this over `user.roles = ['admin']` i.e. dojomouse's answer? – Dan Jun 19 '15 at 20:24
  • 1
    @dojomouse 's answer makes assumptions about how `allaning:roles` works. What if they don't store the roles in an array of strings? What if some other piece of code modifies the roles? What if you update `roles` and he adds a way to do this? In the comment to dojomouse's solution, you'll actually seem an example of how if you modify this array directly, you'll 100% break something. This way, you are guaranteed that as long as `addUsersToRoles` doesn't change, you won't bork something else. – DoctorPangloss Jun 19 '15 at 21:04
  • This is much more convoluted than Joe's answer above, which doesn't require manually setting the user id, adding another unnecessary dependency, or as many lines of code, though it presents a neat observer pattern for more complex functionality that might be needed post onCreateUser. – Ruby_Pry Oct 19 '15 at 21:06
  • This answer works when you need to use groups. The other option mentioned by @alanning in the comment http://stackoverflow.com/questions/22649600/unable-to-add-roles-to-user-with-meteor-using-roles-package/22650399#comment49975151_22650399 doesn't work with groups. – Samudra Mar 11 '16 at 08:41
6

The accepted answer forces you to write boilerplate code for login logic given by meteor through accounts-ui/password. The other answers make assumptions about the underlying implementation and the timeout solution introduces a race condition.

Why not do this:

Accounts.onCreateUser(function(options, user) {

    ...

    Meteor.setTimeout(function () {
        Roles.addUsersToRoles(user._id, roles, group);
    },0);
    ...
  
});

You effectively add the role after whatever it is that triggered the onCreateUser call and use alanning's api to add to roles. (Tested with meteor 1.0, roles 1.2.13)

Joe
  • 11,147
  • 7
  • 49
  • 60
  • what's the relevance of using the setTimeout() function here? – jlouzado Sep 04 '15 at 14:37
  • 1
    At the point of onCreateUser, the user has not actually been created. As javascript is a single threaded process, `Meteor.setTimeout` guarantees that the function within is called *after* the current thread in which the user is being created. – Joe Sep 09 '15 at 02:01
  • 1
    I think `Meteor.defer()` has some advantage over `setTimeout` and `0`, but I'm not 100% sure what that advantage is. :) – chmac Sep 29 '15 at 17:45
  • No, Meteor.defer() is just syntactic sugar and nothing more. See https://github.com/meteor/meteor/issues/2176 – nilsi Oct 18 '15 at 05:25
5

I don't understand how to integrate Roles.addUsersToRoles with the onCreateUser function called when a user is created. It doesn't work when it's called within OnCreateUser as you've done, as you've found. But the example case of calling addUsersToRoles within a user creation loop doesn't seem applicable to the normal use case of a new user creating an account.

Instead, I just do:

Accounts.onCreateUser(function(options, user){
  var role = ['admin'];
  user.roles = role
  return user;
});
dojomouse
  • 59
  • 1
  • 1
    While this works for roles, it doesn't work if you have roles and groups at the same time. Normally you'd get something like this: `"roles" : { "__global_roles__" : [ "admin" ] }, ` – d4nyll Feb 18 '15 at 17:06
  • Would this work if `'admin'` wasn't yet in the `roles` collection? Would this add it to the `roles` collection? – Dan Jun 21 '15 at 11:31
2

The issue here really comes down to looking for a post create user hook (which onCreateUser is not).

It turns out, such a thing exists! It's called the postSignUpHook.

https://github.com/meteor-useraccounts/core/blob/master/Guide.md#options

I found this from this SO answer:

https://stackoverflow.com/a/34114000/3221576

This is the preferred method for adding roles to a user created with the boilerplate UserAccounts package (i.e. if you're not rolling your own with Accounts.createUser).

UPDATE

Ultimately I ended up using matb33:collection-hooks as per this comment:

https://github.com/alanning/meteor-roles/issues/35#issuecomment-198979601

Just stick that code in your Meteor.startup and things work as expected.

Community
  • 1
  • 1
KCE
  • 1,159
  • 8
  • 25
1

I didn't want to rewrite logic given by Meteor's accounts packages, set the ID myself, or use setTimeout.

Instead I have Tracker code watching the Meteor.user() call, which returns the user if logged in and null if not.

So, effectively, once a user has logged in, this code will check to see if they've been assigned a role and if they haven't add them.

I did have to use a Meteor method because I don't want roles to be messed with client side, but you can change that.

/* client */
Tracker.autorun(function () {
  var user = Meteor.user();
  if (user && !Roles.getRolesForUser(user).length) {
    Meteor.call('addUserRole', user);
  }
});

/* server */
Meteor.methods({
  addUserRole: function (user) {
    Roles.addUsersToRoles(user, user.profile.edmodo.type);
  },
});
Merlin -they-them-
  • 2,731
  • 3
  • 22
  • 39
0

I way I found to do this and it's pretty simple.

I am passing role as an array, so I used [role]

On client-side, when the user signs up:

var role = $(event.target).find('#role').val();

/*
 * Add account - Client-Side
 */
Accounts.createUser(account, (error) => {
  if (error) {
    console.log(error);
  } else {
    // server-side call to add the user to the role
    // notice Meteor.userId() and [role]
    Meteor.call('assignUserToRole', Meteor.userId(), [role]);

    // everything went well, redirect to home
    FlowRouter.go('home');
  }
});

In our meteor methods (I am using lodash intersect method to verify the role I want user to choose)

  Meteor.methods({
    // when a account is created make sure we limit the roles
    assignUserToRole: (userId, roles) => {
      check(userId, String);
      check(roles, Array);

      var allowedRoles = ['student', 'parent', 'teacher'];
      if (_.intersection(allowedRoles, roles).length > 0) {
        Roles.addUsersToRoles(userId, roles, 'user');
      } else {
        Roles.addUsersToRoles(userId, ['student'], 'user');
      }
    }
  });