///
/// MANAGING USER OBJECTS
///
// Updates or creates a user after we authenticate with a 3rd party.
//
// @param serviceName {String} Service name (eg, twitter).
// @param serviceData {Object} Data to store in the user's record
// under services[serviceName]. Must include an "id" field
// which is a unique identifier for the user in the service.
// @param options {Object, optional} Other options to pass to insertUserDoc
// (eg, profile)
// @returns {Object} Object with token and id keys, like the result
// of the "login" method.
//
Accounts.updateOrCreateUserFromExternalService = function(
serviceName, serviceData, options) {
options = _.clone(options || {});
if (serviceName === "password" || serviceName === "resume")
throw new Error(
"Can't use updateOrCreateUserFromExternalService with internal service "
+ serviceName);
if (!_.has(serviceData, 'id'))
throw new Error(
"Service data for service " + serviceName + " must include id");
// Look for a user with the appropriate service user id.
var selector = {};
var serviceIdKey = "services." + serviceName + ".id";
// XXX Temporary special case for Twitter. (Issue #629)
// The serviceData.id will be a string representation of an integer.
// We want it to match either a stored string or int representation.
// This is to cater to earlier versions of Meteor storing twitter
// user IDs in number form, and recent versions storing them as strings.
// This can be removed once migration technology is in place, and twitter
// users stored with integer IDs have been migrated to string IDs.
if (serviceName === "twitter" && !isNaN(serviceData.id)) {
selector["$or"] = [{},{},{}];
selector["$or"][0][serviceIdKey] = serviceData.id;
selector["$or"][1][serviceIdKey] = parseInt(serviceData.id, 10);
selector["$or"][2]["emails.address"] = serviceData.email;
} else {
selector["$or"] = [{},{}]
selector["$or"][0][serviceIdKey] = serviceData.id;
selector["$or"][1]["emails.address"] = serviceData.email;
}
var user = Meteor.users.findOne(selector);
if (user) {
pinEncryptedFieldsToUser(serviceData, user._id);
// We *don't* process options (eg, profile) for update, but we do replace
// the serviceData (eg, so that we keep an unexpired access token and
// don't cache old email addresses in serviceData.email).
// XXX provide an onUpdateUser hook which would let apps update
// the profile too
var setAttrs = {};
_.each(serviceData, function(value, key) {
setAttrs["services." + serviceName + "." + key] = value;
});
// XXX Maybe we should re-use the selector above and notice if the update
// touches nothing?
Meteor.users.update(user._id, {$set: setAttrs});
return {
type: serviceName,
userId: user._id
};
} else {
// Create a new user with the service data. Pass other options through to
// insertUserDoc.
user = {services: {}};
user.services[serviceName] = serviceData;
return {
type: serviceName,
userId: Accounts.insertUserDoc(options, user)
};
}
};
var OAuthEncryption = Package["oauth-encryption"] && Package["oauth-encryption"].OAuthEncryption;
// OAuth service data is temporarily stored in the pending credentials
// collection during the oauth authentication process. Sensitive data
// such as access tokens are encrypted without the user id because
// we don't know the user id yet. We re-encrypt these fields with the
// user id included when storing the service data permanently in
// the users collection.
//
var pinEncryptedFieldsToUser = function (serviceData, userId) {
_.each(_.keys(serviceData), function (key) {
var value = serviceData[key];
if (OAuthEncryption && OAuthEncryption.isSealed(value))
value = OAuthEncryption.seal(OAuthEncryption.open(value), userId);
serviceData[key] = value;
});
};