fix accounts package detection to work again

This commit is contained in:
David Glasser
2013-07-24 11:54:07 -07:00
parent 99a0273fb7
commit a57a40ea2c
32 changed files with 285 additions and 262 deletions

View File

@@ -0,0 +1,27 @@
Accounts.oauth.registerService('facebook');
if (Meteor.isClient) {
Meteor.loginWithFacebook = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Facebook.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on
// localhost). https://developers.facebook.com/docs/concepts/login/access-tokens-and-types/,
// "Sharing of Access Tokens"
forLoggedInUser: ['services.facebook'],
forOtherUsers: [
// https://www.facebook.com/help/167709519956542
'services.facebook.id', 'services.facebook.username', 'services.facebook.gender'
]
});
}

View File

@@ -1,10 +0,0 @@
Meteor.loginWithFacebook = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Facebook.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,13 +0,0 @@
Accounts.oauth.registerService('facebook');
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on
// localhost). https://developers.facebook.com/docs/concepts/login/access-tokens-and-types/,
// "Sharing of Access Tokens"
forLoggedInUser: ['services.facebook'],
forOtherUsers: [
// https://www.facebook.com/help/167709519956542
'services.facebook.id', 'services.facebook.username', 'services.facebook.gender'
]
});

View File

@@ -11,6 +11,5 @@ Package.on_use(function(api) {
api.add_files('facebook_login_button.css', 'client');
api.add_files('facebook_server.js', 'server');
api.add_files('facebook_client.js', 'client');
api.add_files("facebook.js");
});

View File

@@ -0,0 +1,23 @@
Accounts.oauth.registerService('github');
if (Meteor.isClient) {
Meteor.loginWithGithub = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Github.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// not sure whether the github api can be used from the browser,
// thus not sure if we should be sending access tokens; but we do it
// for all other oauth2 providers, and it may come in handy.
forLoggedInUser: ['services.github'],
forOtherUsers: ['services.github.username']
});
}

View File

@@ -1,10 +0,0 @@
Meteor.loginWithGithub = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Github.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,9 +0,0 @@
Accounts.oauth.registerService('github');
Accounts.addAutopublishFields({
// not sure whether the github api can be used from the browser,
// thus not sure if we should be sending access tokens; but we do it
// for all other oauth2 providers, and it may come in handy.
forLoggedInUser: ['services.github'],
forOtherUsers: ['services.github.username']
});

View File

@@ -11,6 +11,5 @@ Package.on_use(function(api) {
api.add_files('github_login_button.css', 'client');
api.add_files('github_server.js', 'server');
api.add_files('github_client.js', 'client');
api.add_files("github.js");
});

View File

@@ -0,0 +1,31 @@
Accounts.oauth.registerService('google');
if (Meteor.isClient) {
Meteor.loginWithGoogle = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Google.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
forLoggedInUser: _.map(
// publish access token since it can be used from the client (if
// transmitted over ssl or on
// localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent
// refresh token probably shouldn't be sent down.
Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token
function (subfield) { return 'services.google.' + subfield; }),
forOtherUsers: _.map(
// even with autopublish, no legitimate web app should be
// publishing all users' emails
_.without(Google.whitelistedFields, 'email', 'verified_email'),
function (subfield) { return 'services.google.' + subfield; })
});
}

View File

@@ -1,10 +0,0 @@
Meteor.loginWithGoogle = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Google.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,17 +0,0 @@
Accounts.oauth.registerService('google');
Accounts.addAutopublishFields({
forLoggedInUser: _.map(
// publish access token since it can be used from the client (if
// transmitted over ssl or on
// localhost). https://developers.google.com/accounts/docs/OAuth2UserAgent
// refresh token probably shouldn't be sent down.
Google.whitelistedFields.concat(['accessToken', 'expiresAt']), // don't publish refresh token
function (subfield) { return 'services.google.' + subfield; }),
forOtherUsers: _.map(
// even with autopublish, no legitimate web app should be
// publishing all users' emails
_.without(Google.whitelistedFields, 'email', 'verified_email'),
function (subfield) { return 'services.google.' + subfield; })
});

View File

@@ -12,6 +12,5 @@ Package.on_use(function(api) {
api.add_files('google_login_button.css', 'client');
api.add_files('google_server.js', 'server');
api.add_files('google_client.js', 'client');
api.add_files("google.js");
});

View File

@@ -0,0 +1,25 @@
Accounts.oauth.registerService('meetup');
if (Meteor.isClient) {
Meteor.loginWithMeetup = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Meetup.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on
// localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit
forLoggedInUser: ['services.meetup'],
forOtherUsers: ['services.meetup.id']
});
}

View File

@@ -1,10 +0,0 @@
Meteor.loginWithMeetup = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Meetup.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,11 +0,0 @@
Accounts.oauth.registerService('meetup');
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on
// localhost). http://www.meetup.com/meetup_api/auth/#oauth2implicit
forLoggedInUser: ['services.meetup'],
forOtherUsers: ['services.meetup.id']
});

View File

@@ -11,6 +11,5 @@ Package.on_use(function(api) {
api.add_files('meetup_login_button.css', 'client');
api.add_files('meetup_server.js', 'server');
api.add_files('meetup_client.js', 'client');
api.add_files("meetup.js");
});

View File

@@ -0,0 +1,24 @@
Accounts.oauth = {};
var services = {};
// Helper for registering OAuth based accounts packages.
// On the server, adds an index to the user collection.
Accounts.oauth.registerService = function (name) {
if (_.has(services, name))
throw new Error("Duplicate service: " + name);
services[name] = true;
if (Meteor.server) {
// Accounts.updateOrCreateUserFromExternalService does a lookup by this id,
// so this should be a unique index. You might want to add indexes for other
// fields returned by your service (eg services.github.login) but you can do
// that in your app.
Meteor.users._ensureIndex('services.' + name + '.id',
{unique: 1, sparse: 1});
}
};
Accounts.oauth.serviceNames = function () {
return _.keys(services);
};

View File

@@ -7,18 +7,8 @@ Accounts.oauth.registerService = function (name) {
// that in your app.
Meteor.users._ensureIndex('services.' + name + '.id',
{unique: 1, sparse: 1});
};
// For test cleanup only. (Mongo has a limit as to how many indexes it can have
// per collection.)
Accounts.oauth._unregisterService = function (name) {
var index = {};
index['services.' + name + '.id'] = 1;
Meteor.users._dropIndex(index);
};
// Listen to calls to `login` with an oauth option set. This is where
// users actually get logged in to meteor via oauth.
Accounts.registerLoginHandler(function (options) {

View File

@@ -13,6 +13,7 @@ Package.on_use(function (api) {
api.imply('accounts-base', ['client', 'server']);
api.use('oauth', 'server');
api.add_files('oauth_common.js');
api.add_files('oauth_client.js', 'client');
api.add_files('oauth_server.js', 'server');
});

View File

@@ -15,6 +15,5 @@ Package.on_use(function(api) {
api.add_files('twitter_login_button.css', 'client');
api.add_files('twitter_server.js', 'server');
api.add_files('twitter_client.js', 'client');
api.add_files("twitter.js");
});

View File

@@ -0,0 +1,25 @@
Accounts.oauth.registerService('twitter');
if (Meteor.isClient) {
Meteor.loginWithTwitter = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Twitter.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
var autopublishedFields = _.map(
// don't send access token. https://dev.twitter.com/discussions/5025
Twitter.whitelistedFields.concat(['id', 'screenName']),
function (subfield) { return 'services.twitter.' + subfield; });
Accounts.addAutopublishFields({
forLoggedInUser: autopublishedFields,
forOtherUsers: autopublishedFields
});
}

View File

@@ -1,10 +0,0 @@
Meteor.loginWithTwitter = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Twitter.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,11 +0,0 @@
Accounts.oauth.registerService('twitter');
var autopublishedFields = _.map(
// don't send access token. https://dev.twitter.com/discussions/5025
Twitter.whitelistedFields.concat(['id', 'screenName']),
function (subfield) { return 'services.twitter.' + subfield; });
Accounts.addAutopublishFields({
forLoggedInUser: autopublishedFields,
forOtherUsers: autopublishedFields
});

View File

@@ -23,6 +23,95 @@ Template._loginButtons.preserve({
'input[id]': Spark._labelFromIdOrName
});
//
// helpers
//
displayName = function () {
var user = Meteor.user();
if (!user)
return '';
if (user.profile && user.profile.name)
return user.profile.name;
if (user.username)
return user.username;
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
};
// returns an array of the login services used by this app. each
// element of the array is an object (eg {name: 'facebook'}), since
// that makes it useful in combination with handlebars {{#each}}.
//
// don't cache the output of this function: if called during startup (before
// oauth packages load) it might not include them all.
//
// NOTE: It is very important to have this return password last
// because of the way we render the different providers in
// login_buttons_dropdown.html
getLoginServices = function () {
var self = this;
// First look for OAuth services.
var services = Package['accounts-oauth'] ? Accounts.oauth.serviceNames() : [];
// Be equally kind to all login services. This also preserves
// backwards-compatibility. (But maybe order should be
// configurable?)
services.sort();
// Add password, if it's there; it must come last.
if (hasPasswordService())
services.push('password');
return _.map(services, function(name) {
return {name: name};
});
};
hasPasswordService = function () {
return !!Package['accounts-password'];
};
dropdown = function () {
return hasPasswordService() || getLoginServices().length > 1;
};
// XXX improve these. should this be in accounts-password instead?
//
// XXX these will become configurable, and will be validated on
// the server as well.
validateUsername = function (username) {
if (username.length >= 3) {
return true;
} else {
loginButtonsSession.errorMessage("Username must be at least 3 characters long");
return false;
}
};
validateEmail = function (email) {
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true;
if (email.indexOf('@') !== -1) {
return true;
} else {
loginButtonsSession.errorMessage("Invalid email");
return false;
}
};
validatePassword = function (password) {
if (password.length >= 6) {
return true;
} else {
loginButtonsSession.errorMessage("Password must be at least 6 characters long");
return false;
}
};
//
// loginButtonLoggedOut template
//
@@ -81,109 +170,3 @@ Template._loginButtonsMessages.infoMessage = function () {
Template._loginButtonsLoggingInPadding.dropdown = dropdown;
//
// helpers
//
displayName = function () {
var user = Meteor.user();
if (!user)
return '';
if (user.profile && user.profile.name)
return user.profile.name;
if (user.username)
return user.username;
if (user.emails && user.emails[0] && user.emails[0].address)
return user.emails[0].address;
return '';
};
// returns an array of the login services used by this app. each
// element of the array is an object (eg {name: 'facebook'}), since
// that makes it useful in combination with handlebars {{#each}}.
//
// NOTE: It is very important to have this return password last
// because of the way we render the different providers in
// login_buttons_dropdown.html
getLoginServices = function () {
var self = this;
var services = [];
// find all methods of the form: `Meteor.loginWithFoo`, where
// `Foo` corresponds to a login service
//
// XXX we should consider having a client-side
// Accounts.oauth.registerService function which records the
// active services and encapsulates boilerplate code now found in
// files such as facebook_client.js. This would have the added
// benefit of allow us to unify facebook_{client,common,server}.js
// into one file, which would encourage people to build more login
// services packages.
_.each(_.keys(Meteor), function(methodName) {
var match;
if ((match = methodName.match(/^loginWith(.*)/))) {
var serviceName = match[1].toLowerCase();
// HACKETY HACK. needed to not match
// Meteor.loginWithToken. See XXX above.
if (Accounts[serviceName])
services.push(match[1].toLowerCase());
}
});
// Be equally kind to all login services. This also preserves
// backwards-compatibility. (But maybe order should be
// configurable?)
services.sort();
// ensure password is last
if (_.contains(services, 'password'))
services = _.without(services, 'password').concat(['password']);
return _.map(services, function(name) {
return {name: name};
});
};
hasPasswordService = function () {
return Accounts.password;
};
dropdown = function () {
return hasPasswordService() || getLoginServices().length > 1;
};
// XXX improve these. should this be in accounts-password instead?
//
// XXX these will become configurable, and will be validated on
// the server as well.
validateUsername = function (username) {
if (username.length >= 3) {
return true;
} else {
loginButtonsSession.errorMessage("Username must be at least 3 characters long");
return false;
}
};
validateEmail = function (email) {
if (passwordSignupFields() === "USERNAME_AND_OPTIONAL_EMAIL" && email === '')
return true;
if (email.indexOf('@') !== -1) {
return true;
} else {
loginButtonsSession.errorMessage("Invalid email");
return false;
}
};
validatePassword = function (password) {
if (password.length >= 6) {
return true;
} else {
loginButtonsSession.errorMessage("Password must be at least 6 characters long");
return false;
}
};

View File

@@ -9,6 +9,13 @@ Package.on_use(function (api) {
// Export Accounts (etc) to packages using this one.
api.imply('accounts-base', ['client', 'server']);
// Allow us to call Accounts.oauth.serviceNames, if there are any OAuth
// services.
api.use('accounts-oauth', {weak: true});
// Allow us to directly test if accounts-password (which doesn't use
// Accounts.oauth.registerService) exists.
api.use('accounts-password', {weak: true});
api.add_files([
'accounts_ui.js',

View File

@@ -11,6 +11,5 @@ Package.on_use(function(api) {
api.add_files('weibo_login_button.css', 'client');
api.add_files('weibo_server.js', 'server');
api.add_files('weibo_client.js', 'client');
api.add_files("weibo.js");
});

View File

@@ -0,0 +1,23 @@
Accounts.oauth.registerService('weibo');
if (Meteor.isClient) {
Meteor.loginWithWeibo = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Weibo.requestCredential(options, credentialRequestCompleteCallback);
};
} else {
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on localhost)
forLoggedInUser: ['services.weibo'],
forOtherUsers: ['services.weibo.screenName']
});
}

View File

@@ -1,10 +0,0 @@
Meteor.loginWithWeibo = function(options, callback) {
// support a callback without options
if (! callback && typeof options === "function") {
callback = options;
options = null;
}
var credentialRequestCompleteCallback = Accounts.oauth.credentialRequestCompleteHandler(callback);
Weibo.requestCredential(options, credentialRequestCompleteCallback);
};

View File

@@ -1,9 +0,0 @@
Accounts.oauth.registerService('weibo');
Accounts.addAutopublishFields({
// publish all fields including access token, which can legitimately
// be used from the client (if transmitted over ssl or on localhost)
forLoggedInUser: ['services.weibo'],
forOtherUsers: ['services.weibo.screenName']
});

View File

@@ -1,7 +1,7 @@
// A place to store request tokens pending verification
var requestTokens = {};
_Oauth1Test = {requestTokens: requestTokens};
_OAuth1Test = {requestTokens: requestTokens};
// connect middleware
Oauth._requestHandlers['1'] = function (service, query, res) {

View File

@@ -40,7 +40,7 @@ Tinytest.add("oauth1 - loginResultForCredentialToken is stored", function (test)
});
// simulate logging in using twitterfoo
_Oauth1Test.requestTokens[credentialToken] = twitterfooAccessToken;
_OAuth1Test.requestTokens[credentialToken] = twitterfooAccessToken;
var req = {
method: "POST",

View File

@@ -9,8 +9,8 @@ Package.on_use(function (api) {
api.use('oauth', ['client', 'server']);
api.use('underscore', 'server');
api.exportSymbol('OAuth1Binding');
api.exportSymbol('_Oauth1Test');
api.exportSymbol('OAuth1Binding', 'server');
api.exportSymbol('_OAuth1Test', 'server');
api.add_files('oauth1_binding.js', 'server');
api.add_files('oauth1_server.js', 'server');