Meteor Iron Router resubscribe on Rights change with meteor-roles - meteor

I am writing a meteor application with a user-role-system (alanning:roles) My Roles are group based. When a user knows my group url, it is allowed to access the group an get the role "defaultUser" in this group.
localUser is allowed to subscribe to all the local stuff of a group.
based on the group's ID, I also publish some contents.
The Problem is: the Subscription does not resubscribe.
Workflow:
user accesses app,
calls meteor-method to get the default role
gets the default role
subscribes to a publication
the publication (complete-group) publishes the contents if the user has the right role
My Publication looks like:
Meteor.publish "thisGroupPublic", (id) ->
return db.groups.find({_id: id}, {fields: {onlypublicones...}}
Meteor.publishComposite "thisGroupReactive", (id) ->
return {
find: () ->
if !Roles.userIsInRole(#userId, "defaultUser", id)
#ready()
console.log("[thisGroupReactive] => No Rights")
return;
return db.groups.find({_id: id});
children: [
{
find: (group) ->
return db.contents.find({groups: {$in: [group._id]}}, {fields: {apikey: 0}})
}
]
}
The user subscribes the subscription "thisGroupPublic" when the user is on the login page and gets the role "defaultUser" on its first visit to the group as a logged in user. But how do I need to configure iron:router to
resubscribe this subscription that the contents are shown and not only the public stuff?

Say that the user is on a route /something
You have some data that changes and you create a session variable:
Session.set("someDataThatChanges", myChangedData)
Your publish function takes some sort of input, which it uses to return different data from the collection:
Meteor.publish("myCollection", function(input){
return myCollection.find(
// do something here based on 'input'
);
});
Iron Router has a .subscribe method that is the same as Meteor.subscribe and also a subscriptions key that takes a function. You can wrap a Tracker.autorun around your .subscribe and put in your session variable to automatically re-subscribe to something based on the changing value of that session variable.
Router.route("/something", {
name: "templateName",
// a place to put your subscriptions
subscriptions: function() {
console.log("this in router ", this);
Tracker.autorun(function(){
this.subscribe('myCollection', Session.get("someDataThatChanges");
});
},
});

Related

Meteor: how to access another user's details?

Meteor.users.findOne() gives me back my user document.
Meteor.users.findOne({_id: 'my ID'}) gives me back my user document.
Meteor.users.findOne({_id: 'another users's ID'}) gives me back UNDEFINED.
This is obviously restricted by security. But how can I access another users's account details e.g. _id, name, profile, etc?
You'll need to add a publisher for the user. Here's an example:
// The user fields we are willing to publish.
const USER_FIELDS = {
username: 1,
emails: 1,
};
Meteor.publish('singleUser', function (userId) {
// Make sure userId is a string.
check(userId, String);
// Publish a single user - make sure only allowed fields are sent.
return Meteor.users.find(userId, { fields: USER_FIELDS });
});
Then on the client you can subscribe like this:
Metor.subscribe('singleUser', userId);
or use a template subscription like this:
this.subscribe('singleUser', userId);
Security notes:
Always check the arguments to your publishers, or clients can do bad things like pass {} for userId. If you get an error, make sure you meteor add check.
Always use a fields option with the users collection. Otherwise you'll publish all of their secrets. See the "Published Secrets" section of common mistakes.
Run it on the server like so:
Server:
Meteor.publish("otherUsers", function (userID) {
return Meteor.users.findOne({_id: userID});
});
Client:
Meteor.subscribe("otherUsers", <userIdYouWantToGetDetailsFor>);
Then you can just do a Meteor.users.findOne on the client keep in mind you can only do it for your user and the userID that you passed in the meteor subscribe

Meteor observeChanges vs manual update

I have a simple todo schema: (just a sample to draw my question)
{
title: {
type: string
},
value: {
type: string
},
author: {
type: object
},
"author._id": {
type: string
},
"author.firstName": {
type: string
},
"author.lastName": {
type: string
},
}
The author entries are from meteor.user. If the meteor user changes the firstName or lastName i have to update the todo. I have two possibilities:
observerChanges (server side) to users collection and update all todos from this user with the new firstname/lastname
if i call the user update method i can call a method to update all todos
when it's better to use cursor.observeChanges and when it's better to call a update method manual? And why?
As the comment says, you should not store the author name / email in the document if it is mutable:
Store the ID of the user only in the document, the UserID is immutable.
When building your ToDo template, look up the User information by ID: you would need to publish a Publication for user by Id, and subscribe to it on the client with the userId as parameter.
Meteor.publish('userById', function(userId) {
return Meteor.users.find({_id: userId}, {limit:1});
});
in your route / template.onCreated depending on your Router, assuming the document is called doc
this.subscribe('userById', this.doc.author._id);
in the template helper
Template.todoTemplate.helpers({
'Author': function() {
return Meteor.users.findOne({_id: this.doc.author._id});
}
});
and call the Author info in the template
<Template name="todoTemplate">
First Name: {{Author.first_name}}
Last Name: {{Author.last_name}}
</Template>
I think you shouldn't rely on the second method, because sometimes you (or your teammate) might forget to update it. Moreover, if you're denormalizing user data in other collections, users knowing Meteor might just call your Meteor.method or manipulate db from the browser console...
You can use this package:
meteor add matb33:collection-hooks
It adds some hooks to your mongo insert/update/remove call
For example:
Meteor.users.after.update(function (userId, doc, fieldNames, modifier, options) {
if (this.previous.firstName === doc.firstName && this.previous.lastName === doc.lastName) {
return;
}
Todos.update({'author._id': doc._id}, {
$set: {
'author.firstName': doc.firstName,
'author.lastName': doc.lastName,
}
})
}, {fetchPrevious: true})
(To update the Todos collection efficiently, make sure to add index to author field)
This is just a handier way than writing your own observeChanges, and better than manually updating Todos collection every time you update the users collection, because you might forgot to call it in some case, or some hacker user just calls Meteor.users.update(Meteor.userId(), {...}) perhaps...
But still, I think you should always add some auto-correct mechanism to avoid wrong data being displayed, because no matter which method you choose, some error will occur (maybe the server watching the db just crashes right after users update). You can check on the client side when displaying content, if author.firstName doesn't match Meteor.users.findOne(author._id) (but you have to publish the user though...), than call a method to tell the server to update it.

Reactive subscription on user collection

I am trying to subscribe to profdle information of a different user than the logged in user, but I am facing issues as mentioned below
I am using angular-material and my code looks like below:
//publish user info upon following user
Meteor.publish("getUserInfo", function (userId) {
return (Meteor.users.find({_id: userId}, {fields: {profile: 1}}));
});
//subscribe
$scope.$meteorSubscribe("getUserInfo", askLikeController.$root.askLike[0].userId).then(function (subscriptionHandle) {
//Second element in the userProfile array will have the profile of required user
askLikeController.$root.usersProfile = $meteor.collection(Meteor.users, false);
});
Issues:
1. In the variable askLikeController.$root.usersProfile, I am getting both the loggedIn user and the desired userinfo having userId, I was expecting userinfo of only desired userId, why is this?
2. The subscription "getUserInfo" is not reactive, and even the subscription is lost after processing few blocks of code and then in the askLikeController.$root.usersProfile I am left with only user profile of logged in user, my guess is that my subscription is being replaced by inbuilt Meteor subscription for user.
How do I solve the issues?
Regards,
Chidan
First, make sure you have removed autopublish:
> meteor remove autopublish
To get reactivity in angular-meteor you need $meteor.autorun and $scope.getReactively. Here's an example:
// we need the requested id in a scope variable
// anytime the scope var changes, $scope.getReactively will
// ... react!
$scope.reqId = askLikeController.$root.askLike[0].userId;
$meteor.autorun($scope, function() {
$scope.$meteorSubscribe('getUserInfo', $scope.getReactively('reqId')));
}).then(function(){
askLikeController.$root.usersProfile = $meteor.collection(Meteor.users, false);
})
Getting only the user you selected: NOTICE- the logged in users is always published. So you need to specify which user you want to look at on the client side, just like you did on the publish method. So, in the subscribe method:
askLikeController.$root.usersProfile = $meteor.collection(function() {
return Meteor.Users.find({_id: $scope.getReactively('reqId')})
}, false);
At this point you might be better off changing it to an object rather than a collection:
askLikeController.$root.usersProfile = $scope.$meteorObject(Meteor.Users, {_id: $scope.getReactively('reqId')});

Publication of items where User is in group (Alanning Roles and Publications)

I am using Alanning Roles to maintain a set of groups/roles for the users of my application. When a user creates an "Application", I generate a new role for them as the app_name + UUID, then add that as a group with the roles of Admin to the user that created it. I can then use the combination of the generated group name plus either the Admin or Viewer roles to determine which Applications the user has rights to see and/or edit.
The issue that I am having is that I can't figure out a good way to get the publication to only publish the things the user should see. I know that, by default at least, publications are not "reactive" in the way the client is, and they they are only reactive for the cursors they return. But, in my code I create the group/role first, add it to the user, then save the "Application", which I thought would rerun my publication, but it did not:
Meteor.publish('myApplications', function(groups) {
if (this.userId) {
console.log('Running myApplications publication...');
console.log('Found roles for user ' + this.userId + ': ', Roles.getGroupsForUser(this.userId));
return Applications.find({group: {$in: Roles.getGroupsForUser(this.userId)}});
} else {
//console.log("Skipping null user");
return null;
}
});
But, contrary to what I thought would happen (the whole publication method would re-run), I am guessing what really happens is that only the Cursor is updates. So for my next attempt, I added the mrt:reactive-publications package and simply got a cursor to the Meteor.users collection for the user, thinking that would "trigger" the publication to re-run when the user gets updated with the new group/role, but that didn't work.
I have this finally working by simply passing in the groups for the user:
Meteor.publish('myApplications', function(groups) {
if (this.userId) {
if (!groups || groups.length === 0) {
groups = Roles.getGroupsForUser(this.userId);
}
console.log('Running myApplications publication...');
console.log('Found roles for user ' + this.userId + ': ', Roles.getGroupsForUser(this.userId));
return Applications.find({group: {$in: groups}});
} else {
//console.log("Skipping null user");
return null;
}
});
And then I just call the publication like Meteor.subscribe('myApplications', Roles.getGroupsForUser(Meteor.userId())) in my route's waitOn, but this would mean that any client could call the same publication and pass in any groups they like, and potentially see documents they were not intended to see. That seems like a pretty large security flaw.
Is there a better way to implement this such that the client would not be able to coax their way to seeing stuff not theirs? I think the only real way would be to gather the groups on the publication side, but then it breaks the reactivity.
After sifting through a bunch of docs and a few very helpful stack posts, this is the alternative I came up with. Works like a charm!
My objective was to publish 'guest' users' info to the group admins for approval/denial of enhanced permissions.
Meteor.publish('groupAdmin', function(groupId) {
// only publish guest users info to group admins
if(Roles.userIsInRole(this.userId, ['group-admin'], groupId)) {
// I can't explain it but it works!
var obj = {key: {$in: ['guest']}};
var query = {};
var key = ('roles.' + groupId);
query[key] = {$in: ['guest']};
return Meteor.users.find(query, {
fields: {
createdAt: 1,
profile: 1
}
});
} else {
this.stop();
return;
}
});
Reference: How to set mongo field from variable
& How do I use a variable as a field name in a Mongo query in Meteor?

What is the correct way to selectively publish in Meteor based on information in the database?

For instance, when your permissions are group-based, and your user document has a list of groups that the user belongs to. I'm publishing docs in an Items collection, and you should only be able to view items with a groupOwner field matching a group you belong to.
It would be nice if you could autorun inside a publish, but I doubt you can:
Meteor.publish 'screened-items', ->
Deps.autorun ->
user = Users.findOne #userId
return Items.find {groupOwner: {$in: user.groups}}
If you can't, this is the best I can come up with, but it's going to be slow and memory-intensive. Is this the only way to do it?
Meteor.publish 'screened-items', ->
user = Users.findOne #userId
# (hope that the db doesn't change between this line and the observeChanges)
saved_items = Items.find({groupOwner: {$in: user.groups}}).fetch()
# call #added on each item
handle = Users.findOne(#userId).observeChanges {
changed: (_, fields) =>
if fields.groups
new_items = Items.find({groupOwner: {$in: fields.groups}}).fetch()
# compare new_items to saved_items, and call #added() or #removed() for each difference
}
#ready()
#.onStop ->
handle.stop()
You can achieve this two ways:
Use the publish-with-relations package, for example:
Meteor.publish 'screend-items', ->
# select the current user
Meteor.publishWithRelations
handle: this
collection: Meteor.users
filter:
_id: #userId
options:
fields:
groups: 1
mappings: [
key: 'groupOwner' # and map to the `groupOwner` field on Items
collection: Items
]
Denormalize the relationship, providing a succinct list of users to use for publishing
Items._ensureIndex(userIds: 1) # best to index this field
# basic publications
Meteor.publish 'screend-items', ->
# don't expose `userIds` to the client
return Items.find({userIds: #userId}, {fields: userIds: false})
If you want the published docs to change when the userId changes, that is the default behaviour.
However, if the logged-in user changes, the publish function is rerun with the new value. - from docs.meteor.com.
Deps.autorun() only works on the client while Meteor.publish() only works on the server. So you can not autorun inside of publish.
If you are okay to let the client see the 'groups' they're in, the code is a bit simpler because you can start and stop the subscription when the groups change. Like this:
//on client
Deps.autorun( function() {
Meteor.subscribe( 'items', Meteor.user().groups );
});
//on server
Meteor.publish( 'items', function( groups ){
var self = this;
var user = Meteor.users.findOne( {_id: self.userId});
if ( ! (user && user.groups === groups) )
return;
return Items.find({groupOwner: {$in: groups}});
});
Otherwise you would need use two observers inside the publish function - one to watch user for changes to groups and another to manage publishing items that are in the group. See this example of doing a join of collections that way.

Resources