unable to add roles to user with meteor using 'roles' package
Asked Answered
A

7

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");
    }
  }
});
Amye answered 26/3, 2014 at 1:11 Comment(0)
K
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.

Kakalina answered 26/3, 2014 at 2:31 Comment(11)
Excellent answer. I've seen this question come up before and am open to suggestions. Perhaps just calling this out specifically in the docs?Cabbageworm
@Cabbageworm 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.Kakalina
What about users created using accounts-ui?Politick
@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.Kakalina
Wouldn't it be better to put the role assignment part into the callback for Accounts.createUser?Pageantry
@Pageantry - The OP is creating the user on the server and the createUser callback is client only (docs).Kakalina
@iah.vector @heister @Kakalina 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.Esquibel
@Cabbageworm 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?Alford
@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 @Kakalina suggests. I added an example of a new function that sets the roles directly on the user object here 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).Cabbageworm
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.Cabbageworm
Are you guys missing Meteor.setTimeout? I tried it out and it seems to work fine. Is there any pitfalls with this method?Rentier
E
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;
});
Esquibel answered 16/5, 2015 at 4:18 Comment(4)
What is the advantage of this over user.roles = ['admin'] i.e. dojomouse's answer?Alford
@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.Esquibel
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.Upside
This answer works when you need to use groups. The other option mentioned by @Cabbageworm in the comment #22650100 doesn't work with groups.Pacifa
R
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)

Rentier answered 26/6, 2015 at 4:31 Comment(4)
what's the relevance of using the setTimeout() function here?Marilumarilyn
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.Rentier
I think Meteor.defer() has some advantage over setTimeout and 0, but I'm not 100% sure what that advantage is. :)Write
No, Meteor.defer() is just syntactic sugar and nothing more. See github.com/meteor/meteor/issues/2176Stevenson
I
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;
});
Isopod answered 26/4, 2014 at 23:4 Comment(2)
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" ] }, Flin
Would this work if 'admin' wasn't yet in the roles collection? Would this add it to the roles collection?Alford
W
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://mcmap.net/q/529511/-is-there-a-post-createuser-hook-in-meteor-when-using-accounts-ui-package

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.

Whether answered 27/4, 2016 at 14:47 Comment(1)
that is the kind of solution i was looking for!Keverne
V
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);
  },
});
Valerivaleria answered 4/9, 2015 at 1:47 Comment(0)
W
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');
      }
    }
  });
Wadsworth answered 19/2, 2016 at 1:21 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.