So Meteor has this great Accounts package which allows for easy login via password or other services. However, I'm currently creating a web service with multiple services required (facebook / twitter / etc). The link here: How to add External Service logins to an already existing account in Meteor? suggests a "hack" by creating duplicate accounts and just merging the data, but it seems very unsatisfactory to me.
So my questions are:
Thanks for your help.
I've solved the problem by reverse engineering the accounts-oauth-X
packages! The optimal solution that handles all the edge cases is to have one story for logins, and another for explicit associations. Here are the solutions written up in literate coffeescript.
Make sure you have the required packages:
meteor add google
meteor add facebook
meteor add oauth
This autojoins accounts if they have the same email. Best used for login because it is robust. However, isn't good for explicit associations because if the email addresses are different, then it won't work. (And a manual merge will log the user out and force you to log back in). For explicitly associating an account, even if it has a different email, see the solution underneath.
Accounts.onCreateUser (options, user) ->
user.city = null
user.networks = []
user.attending =[]
user.profile ?= {}
We need to act extra logic if the user registered via an oauth service, so that we can standardize the location of name and email.
if user.services?
service = _.keys(user.services)[0]
Check if any existing account already has the email associated with the service. If so, just incorporate the user information in.
email = user.services[service].email
if email?
oldUser = Meteor.users.findOne({"emails.address": email})
Make sure the old account has the required services object.
if oldUser?
oldUser.services ?= {}
if service == "google" or service == "facebook"
Merge the new service key into our old user. We also check if there are new emails to add to our user. Then delete the old user from the DB, and just return it again to create it again directly. The supposed new user that was to be created is discarded.
oldUser.services[service] = user.services[service]
Meteor.users.remove(oldUser._id)
user = oldUser
Otherwise just create the user as normal, standardizing the email and name fields.
else
if service == "google" or service == "facebook"
if user.services[service].email?
user.emails = [{address: user.services[service].email, verified: true}]
else
throw new Meteor.Error(500, "#{service} account has no email attached")
user.profile.name = user.services[service].name
return user
This adds services to the services hash on the User record, identical to as if it were created by the built in hash system. The code needs to be client-side (to grab the service oauth token), and part server-side (to make further API calls to grab user data and associate it with the user record)
This function is the entrypoint to adding functions onto our account.
addUserService = (service) ->
We need to use Meteor's built in email verification system if they choose email.
if service == "email"
else
switch service
For standard oauth services, we request the credentials on the client side, and then pass them off to the server to do the further API calls to gather the requisite information and to add that information to our account.
when "facebook"
Facebook.requestCredential(
requestPermissions: ["email", "user_friends", "manage_notifications"],
, (token) ->
Meteor.call "userAddOauthCredentials", token, Meteor.userId(), service, (err, resp) ->
if err?
Meteor.userError.throwError(err.reason)
)
when "google"
Google.requestCredential
requestPermissions: ["email", "https://www.googleapis.com/auth/calendar"]
requestOfflineToken: true,
, (token) ->
Meteor.call "userAddOauthCredentials", token, Meteor.userId(), service, (err, resp) ->
if err?
Meteor.userError.throwError(err.reason)
we just need to set up a simple binding to glue it together.
Template.userAddServices.events
"click button": (e) ->
e.preventDefault()
service = $(event.target).data("service")
addUserService(service)
Here we define server side methods related to the user model.
Meteor.methods
This gets passed the data from the clientside, which grabbed the access token already. Uses the token to discover the rest of the information about the user on that service. It then checks to see if that account is already registered, and if not - adds it to the current account.
userAddOauthCredentials: (token, userId, service) ->
switch service
when "facebook"
data = Facebook.retrieveCredential(token).serviceData
when "google"
data = Google.retrieveCredential(token).serviceData
selector = "services.#{service}.id"
oldUser = Meteor.users.findOne({selector: data.id})
if oldUser?
throw new Meteor.Error(500, "This #{service} account has already" +
"been assigned to another user.")
updateSelector = "services.#{service}"
Meteor.users.update(userId, {$set: {updateSelector: data }})
We can also gleam at the email from this account. If it isn't already on the current user, we can grab it and add it.
if not _.contains(Meteor.user().emails, data.email)
Meteor.users.update(userId, {$push: {"emails": {address: data.email, verified: true} }})
Removing existing user may create issues in some senarios. In my use case I need to merge google account while log in in to the system with another oauth account. This can be achieved with following modifications.
Accounts.onCreateUser(function(options, user) {
if ( user.services )
service = _.keys( user.services )[0]; // get service type
var email = user.services[service].email ;
var oldUser = Meteor.users.findOne({ 'emails.address': email });
if ( oldUser ){
oldUser.services = oldUser.services ? oldUser.services : {};
oldUser.services[service] = user.services[ service ];
return oldUser;
}
return user;
});
userId = Meteor.users.insert(fullUser);
line in Accounts.insertUserDoc function of packages/accounts-base/ accounts_server.js should be replaced with following code. Hope Meteor will fix this. until that have to use a custom package.
if(Meteor.users.findOne(fullUser._id)){
userId = fullUser._id;
delete fullUser._id;
Meteor.users.update(userId, {$set: fullUser});
} else
userId = Meteor.users.insert(fullUser);
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With