How do you conditionally send data to the client in Meteor? - javascript

I'm trying to figure out how to conditionally send data to the client in meteor. I have two user types, and depending on the type of user, their interfaces on the client (and thus the data they require is different).
Lets say users are of type counselor or student. Every user document has something like role: 'counselor' or role: 'student'.
Students have student specific information like sessionsRemaining and counselor, and counselors have things like pricePerSession, etc.
How would I make sure that Meteor.user() on the client side has the information I need, and none extra? If I'm logged in as a student, Meteor.user() should include sessionsRemaining and counselor, but not if I'm logged in as a counselor. I think what I may be searching for is conditional publications and subscriptions in meteor terms.

Use the fields option to only return the fields you want from a Mongo query.
Meteor.publish("extraUserData", function () {
var user = Meteor.users.findOne(this.userId);
var fields;
if (user && user.role === 'counselor')
fields = {pricePerSession: 1};
else if (user && user.role === 'student')
fields = {counselor: 1, sessionsRemaining: 1};
// even though we want one object, use `find` to return a *cursor*
return Meteor.users.find({_id: this.userId}, {fields: fields});
});
And then on the client just call
Meteor.subscribe('extraUserData');
Subscriptions can overlap in Meteor. So what's neat about this approach is that the publish function that ships extra fields to the client works alongside Meteor's behind-the-scenes publish function that sends basic fields, like the user's email address and profile. On the client, the document in the Meteor.users collection will be the union of the two sets of fields.

Meteor users by default are only published with their basic information, so you'll have to add these fields manually to the client by using Meteor.publish. Thankfully, the Meteor docs on publish have an example that shows you how to do this:
// server: publish the rooms collection, minus secret info.
Meteor.publish("rooms", function () {
return Rooms.find({}, {fields: {secretInfo: 0}});
});
// ... and publish secret info for rooms where the logged-in user
// is an admin. If the client subscribes to both streams, the records
// are merged together into the same documents in the Rooms collection.
Meteor.publish("adminSecretInfo", function () {
return Rooms.find({admin: this.userId}, {fields: {secretInfo: 1}});
});
Basically you want to publish a channel that returns certain information to the client when a condition is met, and other info when it isn't. Then you subscribe to that channel on the client.
In your case, you probably want something like this in the server:
Meteor.publish("studentInfo", function() {
var user = Meteor.users.findOne(this.userId);
if (user && user.type === "student")
return Users.find({_id: this.userId}, {fields: {sessionsRemaining: 1, counselor: 1}});
else if (user && user.type === "counselor")
return Users.find({_id: this.userId}, {fields: {pricePerSession: 1}});
});
and then subscribe on the client:
Meteor.subscribe("studentInfo");

Because Meteor.users is a collection like any other Meteor collection, you can actually refine its publicized content like any other Meteor collection:
Meteor.publish("users", function () {
//this.userId is available to reference the logged in user
//inside publish functions
var _role = Meteor.users.findOne({_id: this.userId}).role;
switch(_role) {
case "counselor":
return Meteor.users.find({}, {fields: { sessionRemaining: 0, counselor: 0 }});
default: //student
return Meteor.users.find({}, {fields: { counselorSpecific: 0 }});
}
});
Then, in your client:
Meteor.subscribe("users");
Consequently, Meteor.user() will automatically be truncated accordingly based on the role of the logged-in user.
Here is a complete solution:
if (Meteor.isServer) {
Meteor.publish("users", function () {
//this.userId is available to reference the logged in user
//inside publish functions
var _role = Meteor.users.findOne({ _id: this.userId }).role;
console.log("userid: " + this.userId);
console.log("getting role: " + _role);
switch (_role) {
case "counselor":
return Meteor.users.find({}, { fields: { sessionRemaining: 0, counselor: 0 } });
default: //student
return Meteor.users.find({}, { fields: { counselorSpecific: 0 } });
}
});
Accounts.onCreateUser(function (options, user) {
//assign the base role
user.role = 'counselor' //change to 'student' for student data
//student specific
user.sessionRemaining = 100;
user.counselor = 'Sam Brown';
//counselor specific
user.counselorSpecific = { studentsServed: 100 };
return user;
});
}
if (Meteor.isClient) {
Meteor.subscribe("users");
Template.userDetails.userDump = function () {
if (Meteor.user()) {
var _val = "USER ROLE IS " + Meteor.user().role + " | counselorSpecific: " + JSON.stringify(Meteor.user().counselorSpecific) + " | sessionRemaining: " + Meteor.user().sessionRemaining + " | counselor: " + Meteor.user().counselor;
return _val;
} else {
return "NOT LOGGED IN";
}
};
}
And the HTML:
<body>
<div style="padding:10px;">
{{loginButtons}}
</div>
{{> home}}
</body>
<template name="home">
<h1>User Details</h1>
{{> userDetails}}
</template>
<template name="userDetails">
DUMP:
{{userDump}}
</template>

Related

Structure role-management in meteor-app with alanning:roles

I need some advice for building a correct role schema and management in my meteor-app.
Structure
Im using alanning:roles#1.2.13 for adding role management functionallity to the app.
There are four different user-types: Admin, Editor, Expert and User.
Furthermore there are several modules with different content, i.e. Cars, Maths and Images. Every module is organized in an own meteor-package.
In every module there are several categories, which can be added dynamically by editors.
Categories in modules
Module is structured like this:
elementSchema = new SimpleSchema({
element: {type: String, optional: true}
});
Cars.attachSchema(new SimpleSchema({
title: { type: String },
content: { type: String },
category: { type: [elementSchema], optional: true },
});
As you can see, all available categories are inside of the Collection of the module.
Rights
Admin: Complete rights
Editor: Can edit elements in selected moduls (i.e. editor_1 can edit elements in Cars and Images but not for Maths)
Expert: Can get rights to a complete module or just to some categories of a module (i.e.) expert_1 can edit Images, but only the elements in category "Honda" and "Mercedes" in Cars; no editing to Maths)
User: No editing
This is how I do the authentification technically:
router.js
var filters = {
authenticate: function () {
var user;
if (Meteor.loggingIn()) {
this.layout('login');
this.render('loading');
} else {
user = Meteor.user();
if (!user) {
this.layout('login');
this.render('signin');
return;
}
this.layout('Standard');
this.next();
}
}
}
Router.route('/car/:_id', {
name: 'car',
before: filters.authenticate,
data: function () {
return {
cars: Cars.findOne({ _id: this.params._id })
};
}
});
template
<template name="car">
{{#if isInRole 'cars'}}
Some form for editing
{{else}}
<h1>Restricted area</h1>
{{/if}}
</template>
I put this router.js to every package. Only change is the data function which uses the Collection of each package (Cars, Maths, Images).
Update: As 'Eliezer Steinbock' commented it is necessary to restrict acces to the mongoDB itself. But until now I only did that on the routes.
permissions.js
Cars.allow({
insert: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
},
update: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
}
});
My problems
1) My first problem is how to use roles and groups. What would be the best way for using groups? And the second problem is, that there are no fixed categories in the modules. Right now I have no idea for a useful role/group schema.
2) How do I check for the roles? As there are different roles which can get access: admin, editor and expert. Also I got the problem with these experts who just get access to defined categories of this module.
3) Wouldn't it be better to make the permission.js more general. I mean, is it possible to make a dynamic function, so I don't have to put everywhere the same code? How do I implement the roles in the permission.js in a useful way?
if the logic for the permissions is the same you could just define it once in permissions.js
App = App || {}; // We are using Namespaces, so you don't have to.. but it's good
App.Permissions = {
insert: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
},
update: function(userId) {
var loggedInUser = Meteor.user()
if (loggedInUser && Roles.userIsInRole(loggedInUser, ['admin','editor'])) return true;
}
}
And then you can use it for your Collections:
Cars.allow(App.Permissions); // Or
Cars.allow(App.Permissions.getPermissionsForGroup('cars'))
Define roles somewhere..
Roles
// Give user the role "editor" in "cars" group
Roles.addUsersToRoles(someUserId, ['editor'], 'cars');
Roles.addUsersToRoles(someOtherId, ['admin'], 'cars');
Which you can prepare in permissions.js like this:
Permissions
App = App || {};
App.Permissions = {
insert: function(userId) {...},
update: function(userId) {...},
getPermissionsForGroup: function(group) {
return {
insert: function(userId, doc) {
// Only admin can insert
return Roles.userIsInRole(userId, "admin", group);
},
update: function(userId, doc, fields, modifier) {
// Editor & Admin can edit
return Roles.userIsInRole(userId, ["editor","admin"], group);
},
remove: function(userId, doc) {
// Only admin can remove
return Roles.userIsInRole(userId, "admin", group);
}
}
}
In this example admins can insert and update.. and editors can only update, but insert.
Regarding the documentation of alanning:roles you define and use roles like this:
// Super Admin definition..
Roles.addUsersToRoles(superAdminId, ['admin'], Roles.GLOBAL_GROUP);
Roles.addUsersToRoles(joesUserId, ['manage-team','schedule-game'], 'manchester-united.com')
Roles.addUsersToRoles(joesUserId, ['player','goalie'], 'real-madrid.com')
Roles.userIsInRole(joesUserId, 'manage-team', 'manchester-united.com') // => true
Roles.userIsInRole(joesUserId, 'manage-team', 'real-madrid.com') // => false
Yeah, make sure, that the permission logic will be included before your Collection definition.. obviously :)

Getting Facebook Avatar in Meteor when Autopublish is removed

Currently when auto publish is removed, only {{currentUser.profile.name}} works.I'm trying to get {{currentUser.profile.first_name}} and the avatar from Facebook but have not been able to do so. Here is my code...
On the Server side:
Meteor.publish('userData', function() {
if(!this.userId) return null;
return Meteor.users.find(this.userId, {fields: {
'services.facebook': 1
}});
});
On Iron Router:
Router.configure({
waitOn: function() {
return Meteor.subscribe('userData');
}
});
From my understanding, I see that Meteor is publishing all the userData and then subscribing to it via Iron Router. What I don't understand is why this is not working -- as I think {{currentUser.profile.first_name}} should work but isn't.
Like Richard suggests, when a user is created, you can copy the services document to the profile doc.
Accounts.onCreateUser(function(options, user) {
// We still want the default hook's 'profile' behavior.
if (options.profile) {
user.profile = options.profile;
user.profile.memberSince = new Date();
// Copy data from Facebook to user object
user.profile.facebookId = user.services.facebook.id;
user.profile.firstName = user.services.facebook.first_name;
user.profile.email = user.services.facebook.email;
user.profile.link = user.services.facebook.link;
}
return user;
});
Your publication to get their first name and Facebook ID would look like this...
/* ============== Single User Data =============== */
Meteor.publish('singleUser', function(id) {
check(id, String);
return Meteor.users.find(id,
{fields: {'profile.facebookId': 1, 'profile.name': 1, 'profile.firstName': 1, 'profile.link': 1}});
});
You can access a user's Facebook avatar with a template helper function...
Template.profileView.helpers({
userPicHelper: function() {
if (this.profile) {
var id = this.profile.facebookId;
var img = 'http://graph.facebook.com/' + id + '/picture?type=square&height=160&width=160';
return img;
}
}
});
In your template, you can then use the following helper (provided you are wrapping this in a block that contains user data):
<img src="{{userPicHelper}}" alt="" />
I believe you're trying to access the first_name field from the services subdocument. It should be {{currentUser.services.facebook.first_name}}
If you want to transfer first_name to the profile subdocument, you can have the following event handler:
Accounts.onCreateUser(function(options, user) {
// ... some checks here to detect Facebook login
user.profile.firstName = user.services.facebook.first_name;
user.profile.lastName = user.services.facebook.last_name;
});

Meteor - rendering the name of the owner in a list objects

I'm getting an object not found error when I try and lookup the owner of the objects
i'm trying to render. I'm looping through a collection of video clips, that can be updated or administered by users. The code works fine when I'm logged in, but when I try to use this and I'm logged out, I get "Exception in queued task: TypeError: Cannot read property '_id' of undefined at Object.Template.video_info.creatorName "
I've tried to debug this by doing this:
console.log(this.owner);
var owner = Meteor.users.findOne(this.owner);
console.log(owner);
When I check the console log, I can see that the correct userid is being found, and when i manually run Meteor.users.findOne with this id I get a user object returned. Is there something strange about the timings in Meteor that is preventing this?
UPDATE: If I add a try...catch to the template creatorname function then 2 errors get logged but the template still renders... ??? Seems like this template is being called twice, one when it's not ready, and again once it is. Why would that be.
Example of the try...catch block:
Template.video_info.creatorName = function () {
try{
var owner = Meteor.users.findOne(this.owner);
if (owner._id === Meteor.userId())
return "me";
return displayName(owner);
} catch (e){
console.log(e);
}
};
ORIGINAL BROKEN CODE BELOW THIS POINT
This is in my HTML:
<body>
<div>
{{> video_list}}
</div>
</body>
<template name="video_list">
<h1>Video List</h1>
{{#each videos}}
<ul>
{{> video_info}}
</ul>
{{else}}
No videos yet.
{{/each}}
<div class="footer">
<button>Like!</button>
</div>
</template>
<template name="video_info">
<li class="video-list {{maybe_selected}}">
<img src="{{image}}" />
<div>
<h3>{{title}}</h3>
<p>{{description}}</p>
<h4>{{creatorName}}</h4>
</div>
</li>
</template>
This is in my client.js
Meteor.subscribe("videos");
if (Meteor.isClient) {
Template.video_list.videos = function() {
return Videos.find({}, {sort: {title: 1}});
};
Template.video_list.events = {
'click button': function(){
Videos.update(Session.get('session_video'),{$inc: {likes: 1}});
}
}
Template.video_info.maybe_selected = function() {
return Session.equals('session_video', this._id) ? "selected" : "";
}
Template.video_info.events = {
'click': function(){
Session.set('session_video', this._id);
}
}
Template.video_info.creatorName = function () {
var owner = Meteor.users.findOne(this.owner);
if (owner._id === Meteor.userId())
return "me";
return displayName(owner);
};
}
if (Meteor.isServer) {
Meteor.startup(function () {
// code to run on server at startup
});
}
This is in my model.js
Videos = new Meteor.Collection("videos");
Videos.allow({
insert: function (userId, video) {
return false; // no cowboy inserts -- use createParty method
},
update: function (userId, video, fields, modifier) {
if (userId !== video.owner)
return false; // not the owner
var allowed = ["title", "description", "videoid", "image", "start"];
if (_.difference(fields, allowed).length)
return false; // tried to write to forbidden field
// A good improvement would be to validate the type of the new
// value of the field (and if a string, the length.) In the
// future Meteor will have a schema system to makes that easier.
return true;
},
remove: function (userId, video) {
// You can only remove parties that you created and nobody is going to.
return video.owner === userId; //&& attending(video) === 0;
}
});
var NonEmptyString = Match.Where(function (x) {
check(x, String);
return x.length !== 0;
});
var NonEmptyNumber = Match.Where(function (x) {
check(x, Number);
return x.length !== 0;
});
createVideo = function (options) {
var id = Random.id();
Meteor.call('createVideo', _.extend({ _id: id }, options));
return id;
};
Meteor.methods({
// options should include: title, description, x, y, public
createVideo: function (options) {
check(options, {
title: NonEmptyString,
description: NonEmptyString,
videoid: NonEmptyString,
image:NonEmptyString,
start: NonEmptyNumber,
_id: Match.Optional(NonEmptyString)
});
if (options.title.length > 100)
throw new Meteor.Error(413, "Title too long");
if (options.description.length > 1000)
throw new Meteor.Error(413, "Description too long");
if (! this.userId)
throw new Meteor.Error(403, "You must be logged in");
var id = options._id || Random.id();
Videos.insert({
_id: id,
owner: this.userId,
videoid: options.videoid,
image: options.image,
start: options.start,
title: options.title,
description: options.description,
public: !! options.public,
invited: [],
rsvps: []
});
return id;
},
});
///////////////////////////////////////////////////////////////////////////////
// Users
displayName = function (user) {
if (user.profile && user.profile.name)
return user.profile.name;
return user.emails[0].address;
};
var contactEmail = function (user) {
if (user.emails && user.emails.length)
return user.emails[0].address;
if (user.services && user.services.facebook && user.services.facebook.email)
return user.services.facebook.email;
return null;
};
I think I've found the solution to this one. After reading about the caching works in Meteor, I've discovered the subscription model and how this relates to meteors minimongo http://docs.meteor.com/#dataandsecurity. The reason this was failing then succeeding was that on the first load the data is still being cached in minimongo. I'm currently checking against Accounts login Services Configured to check if the user data has been loaded. I'm currently using this because I can't find a way to subscribe to the Metor users service, but my guess is that the Accounts login service would rely on the Metor users collection. My current solution looks like this:
if(Accounts.loginServicesConfigured()){
var owner = Meteor.users.findOne(this.owner);
if (owner._id === Meteor.userId())
return "me";
return displayName(owner);
}
Currently this appears to be working correctly. I'm still delving into how to subscribe to this users service.Couple of really userful resferences I found while searching for a solution for this
https://github.com/oortcloud/unofficial-meteor-faq
http://psychopyko.com/cool-stuff/meteor-6-simple-tips/
https://groups.google.com/forum/#!topic/meteor-talk/QKXe7qfBfqg
The app might not be publishing the user id to the client when you are logged out. You can try calling the find method on the server and return the user. Or use a different key for querying/

Sails.js Set model value with value from Callback

i need to provide something like an association in my Model. So I have a Model called Posts with an userid and want to get the username from this username and display it.
So my ForumPosts.js Model looks like the following:
module.exports = {
schema: true,
attributes: {
content: {
type: 'text',
required: true
},
forumTopicId: {
type: 'text',
required: true
},
userId: {
type: 'integer',
required: true
},
getUsername: function(){
User.findOne(this.userId, function foundUser(err, user) {
var username = user.username;
});
console.log(username);
return username;
}
}
};
I know that this return will not work because it is asynchronus... But how can i display it in my view? At the Moment i retrive the value with:
<%= forumPost.getUsername() %>
And for sure get an undefined return...
So the question is: How can I return this value - or is there a better solution than an instanced Model?
Thanks in advance!
Off the top of my head, you can just load associated user asynchronously before rendering:
loadUser: function(done){
var that = this;
User.findOne(this.userId, function foundUser(err, user) {
if ((err)||(!user))
return done(err);
that.user = user;
done(null);
});
}
then in your controller action:
module.exports = {
index: function(req, res) {
// Something yours…
forumPost.loadUser(function(err) {
if (err)
return res.send(err, 500);
return res.view({forumPost: forumPost});
});
}
}
and in your view:
<%= forumPost.user.username %>
This is kind of a quick and dirty way. For a more solid and long-term solution (which is still in development so far) you can check out the alpha of Sails v0.10.0 with the Associations API.
So this particularly case of associations between your models. So here you have a User model and ForumPost model and you need the user object in place of your user_id as user_id works as a relationship mapping field to your User model.
So if your are using sails V0.9.8 or below you need to handle this logic in your controller where ever you want to access User model attributes in your view.
In your controller write your logic as:
model.export = {
//your getForumPosts method
getForumPosts : function(req,res){
var filters = {};
forumPost.find(filters).done(function(err,posts){
if(err) return res.send(500,err);
// Considering only one post obj
posts = posts[0];
postByUser(posts.user_id,function(obj){
if(obj.status)
{
posts.user = obj.msg;
delete posts.user_id;
res.view({post:posts});
}
else
{
res.send(500,obj.msg);
}
});
}
}
}
function postByUser(user_id,cb){
User.findOne(user_id).done(function(err,user){
if(err) return cb({status:false, msg:err});
if(user){
cb({status:true, msg:user});
}
}
}
and then you can access your post object in your view.
Or else you can keep watch (at GitHub) on next version of sails as they have announced associations in V0.10 n it is in beta testing phase as if now.

Trouble with privileges when adding custom field to a Meteor user

I'm having trouble adding custom user fields to a Meteor user object (Meteor.user). I'd like a user to have a "status" field, and I'd rather not nest it under "profile" (ie, profile.status), which I do know is r/w by default. (I've already removed autopublish.)
I've been able to publish the field to the client just fine via
Meteor.publish("directory", function () {
return Meteor.users.find({}, {fields: {username: 1, status: 1}});
});
...but I can't get set permissions that allow a logged-in user to update their own status.
If I do
Meteor.users.allow({
update: function (userId) {
return true;
}});
in Models.js, a user can edit all the fields for every user. That's not cool.
I've tried doing variants such as
Meteor.users.allow({
update: function (userId) {
return userId === Meteor.userId();
}});
and
Meteor.users.allow({
update: function (userId) {
return userId === this.userId();
}});
and they just get me Access Denied errors in the console.
The documentation addresses this somewhat, but doesn't go into enough detail. What silly mistake am I making?
(This is similar to this SO question, but that question only addresses how to publish fields, not how to update them.)
This is how I got it to work.
In the server I publish the userData
Meteor.publish("userData", function () {
return Meteor.users.find(
{_id: this.userId},
{fields: {'foo': 1, 'bar': 1}}
);
});
and set the allow as follows
Meteor.users.allow({
update: function (userId, user, fields, modifier) {
// can only change your own documents
if(user._id === userId)
{
Meteor.users.update({_id: userId}, modifier);
return true;
}
else return false;
}
});
in the client code, somewhere I update the user record, only if there is a user
if(Meteor.userId())
{
Meteor.users.update({_id: Meteor.userId()},{$set:{foo: 'something', bar: 'other'}});
}
Try:
Meteor.users.allow({
update: function (userId, user) {
return userId === user._id;
}
});
From the documentation for collection.allow:
update(userId, doc, fieldNames, modifier)
The user userId wants to update a document doc. (doc is the current version of the document from the database, without the proposed update.) Return true to permit the change.

Categories

Resources