Meteor: Appending data to a users profile - meteor

I'm creating new users with the following:
Accounts.createUser({
username: t.find('#username').value,
password: t.find('#password').value,
email: t.find('#email').value,
profile : {
name: t.find('#name').value,
division: 'none',
enrolled: 'false'
}
});
I need to be able to update the division field when a user choose from a drop down select. This is my template:
<template name="userProfile">
<p>Select your division</p>
<select id="division">
<option>Div1</option>
<option>Div2</option>
<option>Div3</option>
<option>Div4</option>
</select>
<button id="enrolled"> Not enrolled </button>
<p>a: {{currentUser.profile.name}}</p>
<p>b: {{currentUser.profile.division}}</p>
</template>
I have a click event which works, but what I can't figure out is how to append or modify fields in the profiles:
Template.userProfile.events({
'click #division': function(e, t) {
console.log('Selected division is: ', t.find('#division').value);
Meteor.user.profile.division = t.find('#division').value;
},
'click: #enrolled': function(e, t) {
console.log('User is enrolled? : ');
}
});
What I'd like to happen is when a user chooses a division the field in the currentUser's profile gets updated.
I have one more question but I'll put that in a separate thread.
Thanks.

The code shown will only update a local copy.
To make a permanent change requires a database write to the users collection.
Per the documentation for Meteor.users:
Users are by default allowed to specify their own profile field with
Accounts.createUser and modify it with Meteor.users.update. To allow
users to edit additional fields, use Meteor.users.allow.
If you can't get that working on the client, it is probably a matter of permissions which are enforced on the client and not on the server. Meteor.allow() specifies these permissions on the client, and anything is permitted on the server.
Exploiting that latter fact, another way to implement profile or user information updates is to use remote methods where Meteor.call() on the client sends information to be executed on the server in a function specified in Meteor.Methods(). The server function that you write and put in Meteor.Methods would then test the validity of the update according to any conditions that you might want to defined, and if valid, call Meteor.users.update() with the update to the profile.

Related

Firestore rules and query for document map with email keys to share data with users

Use Case Summary
User A creates a story
User A shares story with unknown (to the app) User B via email (sent via cloud function)
User B receives an email about the story
User B visits app and creates a new account
User B sees/reads story create by User A
Note: stories can only be seen by whom they been shared with or created by
I'm building a role based access system. I've been looking at the role based access firestore documentation and I'm missing one piece.
Consider a story that can only be read by a user for which that story has been shared. Most examples including the firestore example use the UID has the key to identify shared users. However, that user may not currently be a user of the firebase app additionally how does a user assign that UID.
Story Data
{
title: "A Great Story",
roles: {
aliceUID: {
hasRole: true,
type: "owner",
},
bobUID: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.uid}.hasRole`, '==', true)
The second part could potentially be solved by maintaining a separate user collection then you could find the user from their email address, but this doesn't address users that have never logged in.
The user who intends to share a story could add the user with an email address. Then using firebase functions we could send an email to notify the user of the shared story and the user could login to the app and read that story.
If we proceed with this method then you would not have a UID but only an email address as the key.
Story Data
{
title: "A Great Story",
roles: {
alice#yahoo.com: {
hasRole: true,
type: "owner",
},
bob#gmail.com: {
hasRole: true,
type: "reader",
}
}
}
Story Query
firebase.firestore().collection('stories').where(`roles.${user.email}.hasRole`, '==', true)
Updated Firestore Rule - from documentation
function getRole(rsc) {
// Read from the "roles" map in the story document.
return rsc.data.roles[request.auth.uid] || rsc.data.roles[request.auth.token.email];
}
I can not get the email query to work. This SO issue mentions that
Unfortunately dots are not allowed as a map key. So email addresses won't work.
I don't see why this would be a conflict on the rules side. It does make for a likely invalid where clause
e.g.
.where(`roles.${user.email}.hasRole`, '==', true) -> .where(`roles.bob#gmail.com.hasRole`, '==', true)
That looks like invalid JS and unfortunately [ and ] are invalid characters so we can't do
.where(`roles[${user.email}]hasRole`, '==', true)
The final thing I've seen is using for this Firebase talk is to escape the email address using something like
function encodeAsFirebaseKey(string) {
return string.replace(/\%/g, '%25')
.replace(/\./g, '%2E')
.replace(/\#/g, '%23')
.replace(/\$/g, '%24')
.replace(/\//g, '%2F')
.replace(/\[/g, '%5B')
.replace(/\]/g, '%5D');
};
This appears to fix the query where clause and it's a valid data structure, but it's not a valid Firestore rule meaning it has no true security enforcement.
Any ideas on how to implement this? Please include valid data structure, firestore rules, and query. I've shown and seen many examples that get two out of the three which are all non-working solutions.
Thanks!
The basic issue was that I did not know how to properly formulate a valid query. It turns out that you don't need to create a query in one line.
You can use FieldPath to construct your query parameter.
var path = new firebase.firestore.FieldPath('roles', email ,'hasRole');
firebase.firestore().collection('stories').where(path, '==', true)
This solves for the original missing piece.
This is a use case for Control Access with Custom Claims and Security Rules.
The Firebase Admin SDK supports defining custom attributes on user
accounts. This provides the ability to implement various access
control strategies, including role-based access control, in Firebase
apps. These custom attributes can give users different levels of
access (roles), which are enforced in an application's security rules.
User roles can be defined for the following common cases:
Giving a user administrative privileges to access data and resources.
Defining different groups that a user belongs to.
Providing multi-level access:
Differentiating paid/unpaid subscribers.
Differentiating moderators from regular users.
Teacher/student application, etc.
You'll need to stand up a node server (skill level low). A script like below works to generate the claims.
var admin = require('firebase-admin');
var serviceAccount = require("./blah-blah-blah.json");
admin.initializeApp({
credential: admin.credential.cert(serviceAccount),
databaseURL: "https://my-app.firebaseio.com"
});
admin.auth().setCustomUserClaims("9mB3asdfrw34ersdgtCk1", {admin: true}).then(() => {
console.log("Custom Claim Added to UID. You can stop this app now.");
});
Then on your client side, do something like:
firebase.auth().onAuthStateChanged(function(user) {
if (user) {
//is email address up to date? //do we really want to modify it or mess w it?
switch (user.providerData[0].providerId) {
case 'facebook':
case 'github':
case 'google':
case 'twitter':
break;
case 'password':
// if (!verifiedUser) {
// }
break;
}
//if admin
firebase.auth().currentUser.getIdToken().then((idToken) => {
// Parse the ID token.
const payload = JSON.parse(window.atob(idToken.split('.')[1]));
// Confirm the user is an Admin or whatever
if (!!payload['admin']) {
switch (thisPage) {
case "/admin":
showAdminStuff();
break;
}
}
else {
if(isAdminPage()){
document.location.href="/";
}
}
})
.catch((error) => {
console.log(error);
});
}
else {
//USER IS NOT SIGNED IN
}
});
From what I have gathered, you want to make a story private but shareable with anyone. Your biggest concern is for users who do not have the app but have the share link.
And therefore your biggest problem is that the way firebase works means that you cant limit access to your data without using some sort of login.
If you are ok with requiring new users to login, then your answer should just be Dynamic Links. These links are persistent all the way though installation and login which means that anyone can be given a dynamic link that has story access data attached. You would merely need to add a listener to your app's mainActivity or AppDelegate equivalent to record the dynamic link data and run a specif task after login.
If you wish to stay away from the login completely, then you set up the dynamic link to bypass the login process and direct the new-install-user directly to the story. This second option however, requires a bit more work and is less secure because you will probably be forced to duplicate the story data for open access to anyone with the proper link to the story.

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: what is the right way to add custom settings object to users collection?

There are multiple examples on publish/subscribe but not clear on what is the best practice for storing custom data in the in-built "users" collection in Meteor (especially in the new possibility of template specific collections).
For example, I need to store user browse history - something that is accessible through Meteor.user().settings.history.lastvisited[]
The challenge is:
Is any special publish / subscribe required for the above? (the
reason being, I am assuming the users collection is already
published and available on client side - so do we need another?)
How to take care of edge cases where user is new and hence settings.history object may not be defined? Can we have a special publish that automatically takes care of creating an empty object if the settings is undefined? How to do it?
I did this :
// server side
Meteor.publish('userSettings', function (maxRows) {
if (this.userId) {
return Meteor.users.find({ _id: this.userId }, { fields: {'settings':1}});
}
this.ready();
});
//client side
Meteor.subscribe('userSettings');
But I do not see anyway how I can access the published "userSettings" object on the client side - what is missing ??
You can create a field and set it to false/'', on each user you create using the accountsOnCreateUser method.
Accounts.onCreateUser(function(options, user) {
//this function gets called each time a user has been created on the Meteor.user collection
if (options.profile)
user.settings = ''; //this is just and example.
return user;
})
Now the publish looks ok, but in order to get it work im always use a Tracker.autorun function.
Tracker.autorun(function(){
Meteor.subscribe('userSettings');
})
Why the autorun? well if you don't call the auto run here, the subscription get only called 1 time when the apps loads, and not when the user documents.
Take care of yours deny/allow permissions, check this meteor:common mistakes post on the Profile editing section
Also the subscribe function have a callback function. Meteor.subscribe(name, [arg1, arg2...], [callbacks]), so you can do something like this.
var myUserSubscription = Meteor.subscribe('userSettings',function(){
console.log("ok im here on the client side")
console.log("this user subscription is ready " + myUserSubscription.ready())
})
console.log("outside the subscription why not? " + myUserSubscription.ready();
About ready();
True if the server has marked the subscription as ready. A reactive
data source.

Storing per-user data in Meteor

I want to store information for every logged in user in my meteor app, such as their profile picture, bio, ect. But if I try to do something like Meteor.user().picLink = "..."; it appears to get erased on every subsequent call to Meteor.user(). I assume this means I'm not supposed to store extra data directly on the user object like that.
The only response to that that I can think of is to have a separate collection with user data in it. But that seems like it would be hard to keep consistent with Meteor.users. Is there a better way?
All user accounts come with an automatically published profile field which you can update like so:
var userId = Meteor.userId();
var url = 'http://example.com/kittens.jpg';
Meteor.users.update(userId, {$set: {'profile.photo': url});
That will update the underlying database and persist across connections.
As I point out here you should be aware that the profile object is currently editable by default even when the insecure package has been removed. This means any user can open up the console and modify his/her profile.
A better approach is to deny the updates and to use a method instead:
client
var url = 'http://example.com/kittens.jpg';
Meteor.call('update.photo', url);
server
Meteor.users.deny({
update: function() {return true;}
});
Meteor.methods({
'update.photo': function(url) {
check(url, String);
Meteor.users.update(this.userId, {$set: {'profile.photo': url}});
}
});

Is there a post createUser hook in meteor when using accounts-ui package?

Let's say I have a todo app, and I want to make sure that every user that registers has at least one todo to start with, something like "First todo to cross off!", how would I do that in meteor?
In general, the way I see it, I can do it when the user is created for the first time (ideal), or check to see whether they need a new todo every time they log in (less ideal). In the latter case, I can do a check for Todos.findOne(), and if the count is 0, add one. However, seems that whether I do this in my router when the page loads, or on some template's .rendered function, the collection I'm checking hasn't been loaded yet, so I always create a new todo, even if one really does exist. So it'd be great if someone could explain how to get around that.
But, what I'd ideally want is the ability to just create a new Todo when the user is created. There is a Accounts.onCreateUser method, but that is used to add additional info to user profile, not a post-create hook. There's also a method to programmatically create the user using Accounts.createNewUser with a callback, but I'm using the accounts-ui package so am not programmatically adding users. In a less ideal case, I could check for the Todo whenever the user logs in, but even in that case, there seems to be a federated Accounts.loginWithXService login, so not sure how to handle the callback when any user logs in, regardless of service type.
I think I must be missing something simple, so apologies if this is super obvious. Any help is appreciated.
The Meteor API now has the hook onCreateUser:
Accounts.onCreateUser(function (options, user) {
Todos.insert({
owner: user._id,
text: "First todo to cross off!",
});
// We still want the default hook's 'profile' behavior.
if (options.profile)
user.profile = options.profile;
return user;
});
I used the _.wrap method described above but wanted to include an additional suggestion. It's a good idea to call the original callback from your new custom callback. Meteor does some things on the callback that we don't want to miss.
Modified code that worked like a champ for me:
Accounts.createUser = _.wrap(Accounts.createUser, function(createUser) {
// Store the original arguments
var args = _.toArray(arguments).slice(1),
user = args[0];
origCallback = args[1];
var newCallback = function(error) {
// do my stuff
origCallback.call(this, error);
};
createUser(user, newCallback);
});
If you are using the UserAccounts package: postSignUpHook now exists.
Splendido just merged my pull request for exactly this issue.
AccountsTemplates.configure({
/*...*/
postSignUpHook: /*[callback with your actions post full user creation goes here]*/,
/*...*/
}
Documentation (You'll need to scroll down it's the last hook):
func(userId, info) Called, server side only, just after a successfull user account creation, post submitting the pwdForm for sign-up: allows for custom actions on the data being submitted after we are sure a new user was successfully created. A common use might be applying roles to the user, as this is only possible after fully completing user creation in alanning:roles. The userId is available as the first parameter, so that user object may be retrieved. The password is not available as it's already encrypted, though the encrypted password may be found in info if of use.
You can piggyback onto functions that are called by Meteor by wrapping them. I'm also using the accounts-ui and accounts-password packages and I use Underscore's _.wrap method to redefine the loginWithPassword function. Underscore is included in Meteor by default.
I use something like this for logging in:
Meteor.loginWithPassword = _.wrap(Meteor.loginWithPassword, function(login) {
// Store the original arguments
var args = _.toArray(arguments).slice(1),
user = args[0],
pass = args[1],
origCallback = args[2];
// Create a new callback function
// Could also be defined elsewhere outside of this wrapped function
var newCallback = function() { console.info('logged in'); }
// Now call the original login function with
// the original user, pass plus the new callback
login(user, pass, newCallback);
});
In this specific case, the code above would go in your client code somewhere.
For Accounts.createUser, it might look something like this (also somewhere in client code):
Accounts.createUser = _.wrap(Accounts.createUser, function(createUser) {
// Store the original arguments
var args = _.toArray(arguments).slice(1),
user = args[0],
origCallback = args[1];
// Create a new callback function
// Could also be defined elsewhere outside of this wrapped function
// This is called on the client
var newCallback = function(err) {
if (err) {
console.error(err);
} else {
console.info('success');
}
};
// Now call the original create user function with
// the original user object plus the new callback
createUser(user, newCallback);
});
Hope that's helpful.
One of the Meteor devs answered this question in Meteor google group: https://groups.google.com/forum/?fromgroups=#!topic/meteor-talk/KSz7O-tt4w8
Basically, right now, there is no createUser hook when using accounts-ui, only when programmatically doing so via Accounts.createUser. Also, there are no hooks for login, unless using the lower-level login functions like loginWithFacebook, etc. I haven't figured out an ideal way around this yet, but a few ways of handling it:
if needing to enter a default value into a collection, in that collection's subscription, use the onComplete argument. In this callback, if there are no entries in collection, add one. This avoids the first problem I mentioned in my post about not knowing when a collection was loaded, though not ideal since collection could be empty because user already removed first default one:
Meteor.subscribe 'todos', user: Meteor.userId(), () ->
todo = Todos.findOne()
unless todo
Todos.insert user: Meteor.userId()
you can set up a login hook by using the Meteor.autorun reactive method to check for a change in Meteor.userId(). That'll only get called when the user logs in/reloads the page. This is more useful for non-collection stuff since the collection is not guaranteed to be loaded when Meteor.userId is set:
Meteor.autorun () ->
if Meteor.userId()
console.log 'Do some post login hook'
So I think the efficient solution is still out there somewhere, but wanted to update this post with workarounds I had found in the meantime.
I think this answer this question better: How can I create users server side in Meteor?
in resume:
Accounts.createUser({
username: username,
email : email,
password : password,
profile : {
//publicly visible fields like firstname goes here
}
});
check the meteor docs for more: http://docs.meteor.com/#/full/accounts_createuser

Resources