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.
Related
I am trying to create a checking system using firebase, the way it would work is that it would take the business user uid and the then person checking in uid.
The database looks something like this :
Would this work or is there no way to concurrently get two uids?
EDIT
In my haste I forgot to add the code that I tried:
firebase.auth().onAuthStateChanged(function(user) {
if(user) {
var buisnesscheckedin = db.ref("checkedin/" + user.uid)
checkinButton.addEventListener('click', function(e) {
e.preventDefault()
buisnesscheckedin.push({
status: 'true'
})
console.log("Checked In")
})
}
else {
window.location.href = 'index.html'
}
})
This is what I tried but the problem is that I want to be able to get the uid associated with the business instead of using the consumers uid first.
So the path should look this:
checkedin/buisness_uid/consumer_uid/
I'm trying to make a publishment statement to publish
ONLY the author(OP)'s profile avatar. I am thinking of grabbing the _id of the page. And from that page, I will grab the userId which is the author's _id and try to show the profile.
However, I have been very unsuccessful, and currently, I am using the following. Publishing EVERY user's profile avatar.
Publications.js
//Need to filter this to show only OP.
Meteor.publish("userPostAvatar", function() {
return Meteor.users.find( {} ,
{
fields: {'profile.avatar': 1}
})
});
Meteor.publish('singlePost', function(id) {
check(id, String);
return Posts.find(id);
});
Router.js
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('userStatus'),
Meteor.subscribe('userPostAvatar')
];
},
data: function() {
return Posts.findOne({_id:this.params._id});
}
});
You can do a simple join in the userPostAvatar publish function like this:
Meteor.publish('userPostAvatar', function(postId) {
check(postId, String);
var post = Posts.findOne(postId);
return Meteor.users.find(post.authorId, {fields: {profile: 1}});
});
This assumes posts have an authorId field - adjust as needed for your use case. Note three important things:
You will need to subscribe with this.params._id just as you did for singlePost.
The join is non-reactive. If the author changes, the avatar will not be republished. Given the general nature of posts I assume this isn't a problem.
I didn't publish the nested field profile.avatar on purpose because doing so can cause weird behavior on the client. See this question for more details.
I believe you can achieve this within the iron:router data context, by finding the post, associated author (whatever the field is), and then the subsequent user avatar. You can return an object to the iron:router data context. Then you can access post and avatar in the template as variables (so you might need to adjust the template output a little).
Publications.js
Meteor.publish("userPostAvatar", function() {
return Meteor.users.findOne( {} ,
{
fields: {'profile.avatar': 1}
})
});
Meteor.publish('singlePost', function(id) {
check(id, String);
return Posts.find(id);
});
Router.js
Router.route('/posts/:_id', {
name: 'postPage',
waitOn: function() {
return [
Meteor.subscribe('singlePost', this.params._id),
Meteor.subscribe('userStatus'),
Meteor.subscribe('userPostAvatar')
];
},
data: function() {
var post = Posts.findOne({_id: this.params._id});
var avatar = Users.findOne(post.authorId).profile.avatar;
return {
post: post,
avatar: avatar
};
}
});
Two problems with this method are that you could achieve the same thing with template helpers, and the user publication hasn't been limited to one user (I'm unsure how to do this unless we know the authorId within the waitOn, although maybe you could try moving the logic to there instead of the data context as my example shows).
Scenario
I have an app that allows users to create an account, but also allows the user's the ability to delete their account. Upon deletion of their account I have a Cloud Code function that will delete all of the "Post"s the user has made. The cloud code I am using is...
//Delete all User's posts
Parse.Cloud.define("deletePosts", function(request, response) {
var userID = request.params.userID;
var query = new Parse.Query(Parse.Post);
query.equalTo("postedByID", userID);
query.find().then(function (users) {
//What do I do HERE to delete the posts?
users.save().then(function(user) {
response.success(user);
}, function(error) {
response.error(error)
});
}, function (error) {
response.error(error);
});
});
Question
Once I have the query made for all of the user's posts, how do I then delete them? (see: //What do I do HERE?)
You could use
Parse.Object.destroyAll(users); // As per your code – what you call users here are actually posts
See: http://parseplatform.org/Parse-SDK-JS/api/classes/Parse.Object.html#methods_destroyAll
Also, consider using Parse.Cloud.afterDelete on Parse.User (if that is what you mean by "deleting account") to do cleanups such as these.
Oh, and just to be complete, you don't need the save() routine after destroyAll()
Updates in-line below below your "What do I do HERE..." comment:
NOTES:
You don't need to call the save() method, so I took that out.
This, of course, is merely a matter of personal preference, but you may want to choose a parameter name that makes a little more sense than "users", since you're really not querying users, but rather Posts (that just happen to be related to a user).
Parse.Cloud.define("deletePosts", function(request, response) {
var userID = request.params.userID;
var query = new Parse.Query(Parse.Post);
query.equalTo("postedByID", userID);
query.find().then(function (users) {
//What do I do HERE to delete the posts?
users.forEach(function(user) {
user.destroy({
success: function() {
// SUCCESS CODE HERE, IF YOU WANT
},
error: function() {
// ERROR CODE HERE, IF YOU WANT
}
});
});
}, function (error) {
response.error(error);
});
});
Okay, so I am a bit confused about something with Meteor.js. I created a site with it to test the various concepts, and it worked fine. Once I removed "insecure" and "autopublish", I get multiple "access denied" errors when trying to retrieve and push to the server. I belive it has something to do with the following snippet:
Template.posts.posts = function () {
return Posts.find({}, {sort: {time: -1}});
}
I think that it is trying to access the collection directly, which it was allowed to do with "insecure" and "autopublish" enabled, but once they were disabled it was given access denied. Another piece I think is problematic:
else {
Posts.insert({
user: Meteor.user().profile.name,
post: post.value,
time: Date.now(),
});
I think that the same sort of thing is happening: it is trying to access the collection directly, which it is not allowed to do.
My question is, how do I re-factor it so that I do not need "insecure" and "autopublish" enabled?
Thanks.
EDIT
Final:
/**
* Models
*/
Posts = new Meteor.Collection('posts');
posts = Posts
if (Meteor.isClient) {
Meteor.subscribe('posts');
}
if (Meteor.isServer) {
Meteor.publish('posts', function() {
return posts.find({}, {time:-1, limit: 100});
});
posts.allow({
insert: function (document) {
return true;
},
update: function () {
return false;
},
remove: function () {
return false;
}
});
}
Ok, so there are two parts to this question:
Autopublish
To publish databases in meteor, you need to have code on both the server-side, and client-side of the project. Assuming you have instantiated the collection (Posts = new Meteor.Collection('posts')), then you need
if (Meteor.isServer) {
Meteor.publish('posts', function(subsargs) {
//subsargs are args passed in the next section
return posts.find()
//or
return posts.find({}, {time:-1, limit: 5}) //etc
})
}
Then for the client
if (Meteor.isClient) {
Meteor.subscribe('posts', subsargs) //here is where you can pass arguments
}
Insecure
The purpose of insecure is to allow the client to indiscriminately add, modify, and remove any database entries it wants. However, most of the time you don't want that. Once you remove insecure, you need to set up rules on the server detailing who can do what. These two functions are db.allow and db.deny. E.g.
if (Meteor.isServer) {
posts.allow({
insert:function(userId, document) {
if (userId === "ABCDEFGHIJKLMNOP") { //e.g check if admin
return true;
}
return false;
},
update: function(userId,doc,fieldNames,modifier) {
if (fieldNames.length === 1 && fieldNames[0] === "post") { //they are only updating the post
return true;
}
return false;
},
remove: function(userId, doc) {
if (doc.user === userId) { //if the creator is trying to remove it
return true;
}
return false;
}
});
}
Likewise, db.deny will behave the exact same way, except a response of true will mean "do not allow this action"
Hope this answers all your questions
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>