diff --git a/packages/accounts-base/accounts_client.js b/packages/accounts-base/accounts_client.js index ffbecd6938..fa7854b9e2 100644 --- a/packages/accounts-base/accounts_client.js +++ b/packages/accounts-base/accounts_client.js @@ -35,4 +35,30 @@ return Meteor.user(); }); } + + // XXX this can be simplified if we merge in + // https://github.com/meteor/meteor/pull/273 + var loginServicesConfigured = false; + var loginServicesConfiguredListeners = {}; // context.id -> context + Meteor.subscribe("loginServiceConfiguration", function () { + loginServicesConfigured = true; + _.each(loginServicesConfiguredListeners, function(context) { + context.invalidate(); + }); + }); + + // A reactive function returning whether the + // loginServiceConfiguration subscription is ready. Used by + // accounts-ui to hide the login button until we have all the + // configuration loaded + Meteor.accounts.loginServicesConfigured = function () { + if (loginServicesConfigured) + return true; + + // not yet complete, save the context for invalidation once we are. + var context = Meteor.deps.Context.current; + if (context) + loginServicesConfiguredListeners[context.id] = context; + return false; + }; })(); diff --git a/packages/accounts-base/accounts_common.js b/packages/accounts-base/accounts_common.js index 2ce6c2fb96..3aaf9a7062 100644 --- a/packages/accounts-base/accounts_common.js +++ b/packages/accounts-base/accounts_common.js @@ -31,7 +31,17 @@ Meteor.users = new Meteor.Collection( null /*driver*/, true /*preventAutopublish*/); +// Table containing documents with configuration options for each +// login service +Meteor.accounts.configuration = new Meteor.Collection( + "accounts._loginServiceConfiguration", + null /*manager*/, + null /*driver*/, + true /*preventAutopublish*/); + // Thrown when trying to use a login service which is not configured Meteor.accounts.ConfigError = function(description) { this.message = description; }; +Meteor.accounts.ConfigError.prototype = new Error(); +Meteor.accounts.ConfigError.prototype.name = 'Meteor.accounts.ConfigError'; \ No newline at end of file diff --git a/packages/accounts-base/accounts_server.js b/packages/accounts-base/accounts_server.js index 50b8bd0d81..f6519fefd3 100644 --- a/packages/accounts-base/accounts_server.js +++ b/packages/accounts-base/accounts_server.js @@ -193,7 +193,7 @@ /// - /// PUBLISHING USER OBJECTS + /// PUBLISHING DATA /// // Always publish the current user's record to the client. @@ -214,6 +214,23 @@ Meteor.default_server.publish(null, handler, {is_auto: true}); }); + // Publish all login service configuration fields other than secret. + Meteor.publish("loginServiceConfiguration", function () { + return Meteor.accounts.configuration.find({}, {fields: {secret: 0}}); + }); + + // Allow a one-time configuration for a login service. + Meteor.accounts.configuration.allow({}); // disallow mutators + Meteor.methods({ + "configureLoginService": function(options) { + if (!Meteor.accounts.configuration.findOne({service: options.service})) + Meteor.accounts.configuration.insert(options); + else + throw new Meteor.Error(403, "Service " + options.service + " already configured"); + } + }); + + /// /// RESTRICTING WRITES TO USER OBJECTS /// diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 3d34aac30e..d3901c310a 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -1,7 +1,8 @@ (function () { Meteor.loginWithFacebook = function () { - if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.config first"); + var config = Meteor.accounts.configuration.findOne({service: 'facebook'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); var state = Meteor.uuid(); var mobile = /Android|webOS|iPhone|iPad|iPod|BlackBerry/i.test(navigator.userAgent); @@ -13,8 +14,8 @@ scope = Meteor.accounts.facebook._options.scope.join(','); var loginUrl = - 'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId + - '&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook?close' + + 'https://www.facebook.com/dialog/oauth?client_id=' + config.appId + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/facebook?close') + '&display=' + display + '&scope=' + scope + '&state=' + state; Meteor.accounts.oauth.initiateLogin(state, loginUrl); diff --git a/packages/accounts-facebook/facebook_common.js b/packages/accounts-facebook/facebook_common.js index d3ba6e0771..2c1fca99f9 100644 --- a/packages/accounts-facebook/facebook_common.js +++ b/packages/accounts-facebook/facebook_common.js @@ -1,12 +1,7 @@ if (!Meteor.accounts.facebook) { Meteor.accounts.facebook = {}; - Meteor.accounts.facebook._requireConfigs = ['_appId', '_appUrl']; } -Meteor.accounts.facebook.config = function(appId, appUrl, options) { - Meteor.accounts.facebook._appId = appId; - Meteor.accounts.facebook._appUrl = appUrl; +Meteor.accounts.facebook.config = function(options) { Meteor.accounts.facebook._options = options; }; - - diff --git a/packages/accounts-facebook/facebook_configure.html b/packages/accounts-facebook/facebook_configure.html new file mode 100644 index 0000000000..c24b24b823 --- /dev/null +++ b/packages/accounts-facebook/facebook_configure.html @@ -0,0 +1,19 @@ + diff --git a/packages/accounts-facebook/facebook_configure.js b/packages/accounts-facebook/facebook_configure.js new file mode 100644 index 0000000000..26eee7761f --- /dev/null +++ b/packages/accounts-facebook/facebook_configure.js @@ -0,0 +1,10 @@ +Template.configureLoginServicesDialogForFacebook.siteUrl = function () { + return Meteor.absoluteUrl(); +}; + +Template.configureLoginServicesDialogForFacebook.fields = function () { + return [ + {property: 'appId', label: 'App ID'}, + {property: 'secret', label: 'App Secret'} + ]; +}; \ No newline at end of file diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index b24d745e51..0ce7db31c4 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -1,9 +1,5 @@ (function () { - Meteor.accounts.facebook.setSecret = function (secret) { - Meteor.accounts.facebook._secret = secret; - }; - Meteor.accounts.oauth.registerService('facebook', 2, function(query) { var accessToken = getAccessToken(query); @@ -22,13 +18,17 @@ }); var getAccessToken = function (query) { + var config = Meteor.accounts.configuration.findOne({service: 'facebook'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); + // Request an access token var result = Meteor.http.get( "https://graph.facebook.com/oauth/access_token", { params: { - client_id: Meteor.accounts.facebook._appId, - redirect_uri: Meteor.accounts.facebook._appUrl + "/_oauth/facebook?close", - client_secret: Meteor.accounts.facebook._secret, + client_id: config.appId, + redirect_uri: Meteor.absoluteUrl("_oauth/facebook?close"), + client_secret: config.secret, code: query.code } }); diff --git a/packages/accounts-facebook/package.js b/packages/accounts-facebook/package.js index c8ab9ecc19..9b63589120 100644 --- a/packages/accounts-facebook/package.js +++ b/packages/accounts-facebook/package.js @@ -6,6 +6,11 @@ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); api.use('accounts-oauth2-helper', ['client', 'server']); api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['facebook_configure.html', 'facebook_configure.js'], + 'client'); api.add_files('facebook_common.js', ['client', 'server']); api.add_files('facebook_server.js', 'server'); diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index 431b853fe9..97bd9cb7bb 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -1,7 +1,8 @@ (function () { Meteor.loginWithGoogle = function () { - if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first"); + var config = Meteor.accounts.configuration.findOne({service: 'google'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); var state = Meteor.uuid(); @@ -20,9 +21,9 @@ var loginUrl = 'https://accounts.google.com/o/oauth2/auth' + '?response_type=code' + - '&client_id=' + Meteor.accounts.google._clientId + + '&client_id=' + config.clientId + '&scope=' + flat_scope + - '&redirect_uri=' + Meteor.accounts.google._appUrl + '/_oauth/google?close' + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/google?close') + '&state=' + state; Meteor.accounts.oauth.initiateLogin(state, loginUrl); diff --git a/packages/accounts-google/google_common.js b/packages/accounts-google/google_common.js index e5071871bc..b68429d14b 100644 --- a/packages/accounts-google/google_common.js +++ b/packages/accounts-google/google_common.js @@ -1,10 +1,7 @@ if (!Meteor.accounts.google) { Meteor.accounts.google = {}; - Meteor.accounts.google._requireConfigs = ['_clientId', '_appUrl']; } -Meteor.accounts.google.config = function(clientId, appUrl, options) { - Meteor.accounts.google._clientId = clientId; - Meteor.accounts.google._appUrl = appUrl; +Meteor.accounts.google.config = function(options) { Meteor.accounts.google._options = options; }; diff --git a/packages/accounts-google/google_configure.html b/packages/accounts-google/google_configure.html new file mode 100644 index 0000000000..c2d97de2c1 --- /dev/null +++ b/packages/accounts-google/google_configure.html @@ -0,0 +1,28 @@ + diff --git a/packages/accounts-google/google_configure.js b/packages/accounts-google/google_configure.js new file mode 100644 index 0000000000..3ab531e1b6 --- /dev/null +++ b/packages/accounts-google/google_configure.js @@ -0,0 +1,10 @@ +Template.configureLoginServicesDialogForGoogle.siteUrl = function () { + return Meteor.absoluteUrl(); +}; + +Template.configureLoginServicesDialogForGoogle.fields = function () { + return [ + {property: 'clientId', label: 'Client ID'}, + {property: 'secret', label: 'Client secret'} + ]; +}; \ No newline at end of file diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 295010ab38..85903a10be 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -22,12 +22,16 @@ }); var getAccessToken = function (query) { + var config = Meteor.accounts.configuration.findOne({service: 'google'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); + var result = Meteor.http.post( "https://accounts.google.com/o/oauth2/token", {params: { code: query.code, - client_id: Meteor.accounts.google._clientId, - client_secret: Meteor.accounts.google._secret, - redirect_uri: Meteor.accounts.google._appUrl + "/_oauth/google?close", + client_id: config.clientId, + client_secret: config.secret, + redirect_uri: Meteor.absoluteUrl("_oauth/google?close"), grant_type: 'authorization_code' }}); diff --git a/packages/accounts-google/package.js b/packages/accounts-google/package.js index 4db58deaaf..e6484baadb 100644 --- a/packages/accounts-google/package.js +++ b/packages/accounts-google/package.js @@ -6,6 +6,11 @@ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); api.use('accounts-oauth2-helper', ['client', 'server']); api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['google_configure.html', 'google_configure.js'], + 'client'); api.add_files('google_common.js', ['client', 'server']); api.add_files('google_server.js', 'server'); diff --git a/packages/accounts-oauth-helper/oauth_server.js b/packages/accounts-oauth-helper/oauth_server.js index c6f221afe5..15f8e2e6a5 100644 --- a/packages/accounts-oauth-helper/oauth_server.js +++ b/packages/accounts-oauth-helper/oauth_server.js @@ -142,17 +142,9 @@ // Make sure we're configured var ensureConfigured = function(serviceName) { - var service = Meteor.accounts[serviceName]; - - _.each(service._requireConfigs, function(key) { - if (!service[key]) - throw new Meteor.accounts.ConfigError( - "Need to call Meteor.accounts." + serviceName + ".config first"); - }); - - if (Meteor.isServer && !service._secret) - throw new Meteor.accounts.ConfigError( - "Need to call Meteor.accounts." + serviceName + ".setSecret first"); + if (!Meteor.accounts.configuration.findOne({service: serviceName})) { + throw new Meteor.accounts.ConfigError("Service not configured"); + }; }; Meteor.accounts.oauth._renderOauthResults = function(res, query) { diff --git a/packages/accounts-oauth1-helper/oauth1_binding.js b/packages/accounts-oauth1-helper/oauth1_binding.js index 8f03ef8451..835ca74cc1 100644 --- a/packages/accounts-oauth1-helper/oauth1_binding.js +++ b/packages/accounts-oauth1-helper/oauth1_binding.js @@ -116,7 +116,7 @@ OAuth1Binding.prototype._call = function(method, url, headers, params) { }); if (response.error) { - Meteor._debug('Error sending OAuth1 HTTP call', method, url, params, authString); + Meteor._debug('Error sending OAuth1 HTTP call', response.content, method, url, params, authString); throw response.error; } diff --git a/packages/accounts-oauth1-helper/oauth1_server.js b/packages/accounts-oauth1-helper/oauth1_server.js index 3203f08955..4025862d4d 100644 --- a/packages/accounts-oauth1-helper/oauth1_server.js +++ b/packages/accounts-oauth1-helper/oauth1_server.js @@ -7,8 +7,14 @@ // connect middleware Meteor.accounts.oauth1._handleRequest = function (service, query, res) { - var config = Meteor.accounts[service.serviceName]; - var oauthBinding = new OAuth1Binding(config._consumerKey, config._secret, config._urls); + var config = Meteor.accounts.configuration.findOne({service: service.serviceName}); + if (!config) { + throw new Meteor.accounts.ConfigError("Service " + service.serviceName + " not configured"); + } + + var urls = Meteor.accounts[service.serviceName]._urls; + var oauthBinding = new OAuth1Binding( + config.consumerKey, config.secret, urls); if (query.requestTokenAndRedirect) { // step 1 - get and store a request token @@ -20,7 +26,7 @@ Meteor.accounts.oauth1._requestTokens[query.state] = oauthBinding.requestToken; // redirect to provider login, which will redirect back to "step 2" below - var redirectUrl = config._urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; + var redirectUrl = urls.authenticate + '?oauth_token=' + oauthBinding.requestToken; res.writeHead(302, {'Location': redirectUrl}); res.end(); diff --git a/packages/accounts-oauth1-helper/oauth1_tests.js b/packages/accounts-oauth1-helper/oauth1_tests.js index e7a004cc7a..e081ed7c1c 100644 --- a/packages/accounts-oauth1-helper/oauth1_tests.js +++ b/packages/accounts-oauth1-helper/oauth1_tests.js @@ -18,9 +18,9 @@ Tinytest.add("oauth1 - loginResultForState is stored", function (test) { Meteor.accounts.oauth._loginResultForState = {}; Meteor.accounts.oauth._services = {}; + if (!Meteor.accounts.configuration.findOne({service: 'twitterfoo'})) + Meteor.accounts.configuration.insert({service: 'twitterfoo'}); Meteor.accounts.twitterfoo = {}; - Meteor.accounts.twitterfoo._requireConfigs = []; - Meteor.accounts.twitterfoo._secret = 'XXX'; // register a fake login service - twitterfoo Meteor.accounts.oauth.registerService("twitterfoo", 1, function (query) { @@ -78,9 +78,9 @@ Tinytest.add("oauth1 - error in user creation", function (test) { var twitterfailAccessToken = Meteor.uuid(); var twitterfailAccessTokenSecret = Meteor.uuid(); + if (!Meteor.accounts.configuration.findOne({service: 'twitterfail'})) + Meteor.accounts.configuration.insert({service: 'twitterfail'}); Meteor.accounts.twitterfail = {}; - Meteor.accounts.twitterfail._requireConfigs = []; - Meteor.accounts.twitterfail._secret = 'XXX'; // Wire up access token so that verification passes Meteor.accounts.oauth1._requestTokens[state] = twitterfailAccessToken; diff --git a/packages/accounts-oauth2-helper/oauth2_tests.js b/packages/accounts-oauth2-helper/oauth2_tests.js index 8584926d69..56171fec8b 100644 --- a/packages/accounts-oauth2-helper/oauth2_tests.js +++ b/packages/accounts-oauth2-helper/oauth2_tests.js @@ -8,9 +8,9 @@ Tinytest.add("oauth2 - loginResultForState is stored", function (test) { Meteor.accounts.oauth._loginResultForState = {}; Meteor.accounts.oauth._services = {}; + if (!Meteor.accounts.configuration.findOne({service: 'foobook'})) + Meteor.accounts.configuration.insert({service: 'foobook'}); Meteor.accounts.foobook = {}; - Meteor.accounts.foobook._requireConfigs = []; - Meteor.accounts.foobook._secret = 'XXX'; // register a fake login service - foobook Meteor.accounts.oauth.registerService("foobook", 2, function (query) { @@ -49,9 +49,9 @@ Tinytest.add("oauth2 - error in user creation", function (test) { var state = Meteor.uuid(); var failbookId = Meteor.uuid(); + if (!Meteor.accounts.configuration.findOne({service: 'failbook'})) + Meteor.accounts.configuration.insert({service: 'failbook'}); Meteor.accounts.failbook = {}; - Meteor.accounts.failbook._requireConfigs = []; - Meteor.accounts.failbook._secret = 'XXX'; // register a failing login service Meteor.accounts.oauth.registerService("failbook", 2, function (query) { diff --git a/packages/accounts-twitter/package.js b/packages/accounts-twitter/package.js index a144429e23..bae64bbd53 100644 --- a/packages/accounts-twitter/package.js +++ b/packages/accounts-twitter/package.js @@ -6,6 +6,11 @@ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); api.use('accounts-oauth1-helper', ['client', 'server']); api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['twitter_configure.html', 'twitter_configure.js'], + 'client'); api.add_files('twitter_common.js', ['client', 'server']); api.add_files('twitter_server.js', 'server'); diff --git a/packages/accounts-twitter/twitter_client.js b/packages/accounts-twitter/twitter_client.js index b75dcf378e..4e64ed5050 100644 --- a/packages/accounts-twitter/twitter_client.js +++ b/packages/accounts-twitter/twitter_client.js @@ -1,7 +1,8 @@ (function () { Meteor.loginWithTwitter = function () { - if (!Meteor.accounts.twitter._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.twitter.config first"); + var config = Meteor.accounts.configuration.findOne({service: 'twitter'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); var state = Meteor.uuid(); // We need to keep state across the next two 'steps' so we're adding @@ -10,7 +11,7 @@ // url back to app, enters "step 2" as described in // packages/accounts-oauth1-helper/oauth1_server.js - var callbackUrl = Meteor.accounts.twitter._appUrl + '/_oauth/twitter?close&state=' + state; + var callbackUrl = Meteor.absoluteUrl('_oauth/twitter?close&state=' + state); // url to app, enters "step 1" as described in // packages/accounts-oauth1-helper/oauth1_server.js diff --git a/packages/accounts-twitter/twitter_common.js b/packages/accounts-twitter/twitter_common.js index 19fea5f6cb..72d57689fa 100644 --- a/packages/accounts-twitter/twitter_common.js +++ b/packages/accounts-twitter/twitter_common.js @@ -1,13 +1,7 @@ if (!Meteor.accounts.twitter) { Meteor.accounts.twitter = {}; - Meteor.accounts.twitter._requireConfigs = ['_consumerKey', '_appUrl']; } -Meteor.accounts.twitter.config = function(consumerKey, appUrl) { - Meteor.accounts.twitter._consumerKey = consumerKey; - Meteor.accounts.twitter._appUrl = appUrl; -}; - Meteor.accounts.twitter._urls = { requestToken: "https://api.twitter.com/oauth/request_token", authorize: "https://api.twitter.com/oauth/authorize", diff --git a/packages/accounts-twitter/twitter_configure.html b/packages/accounts-twitter/twitter_configure.html new file mode 100644 index 0000000000..a8720a8fc2 --- /dev/null +++ b/packages/accounts-twitter/twitter_configure.html @@ -0,0 +1,13 @@ + diff --git a/packages/accounts-twitter/twitter_configure.js b/packages/accounts-twitter/twitter_configure.js new file mode 100644 index 0000000000..e5e7f05e72 --- /dev/null +++ b/packages/accounts-twitter/twitter_configure.js @@ -0,0 +1,11 @@ +Template.configureLoginServicesDialogForTwitter.siteUrl = function () { + // Twitter doesn't recognize localhost as a domain name + return Meteor.absoluteUrl({replaceLocalhost: true}); +}; + +Template.configureLoginServicesDialogForTwitter.fields = function () { + return [ + {property: 'consumerKey', label: 'Consumer key'}, + {property: 'secret', label: 'Consumer secret'} + ]; +}; \ No newline at end of file diff --git a/packages/accounts-twitter/twitter_server.js b/packages/accounts-twitter/twitter_server.js index ea3d557982..900331d31b 100644 --- a/packages/accounts-twitter/twitter_server.js +++ b/packages/accounts-twitter/twitter_server.js @@ -1,9 +1,5 @@ (function () { - Meteor.accounts.twitter.setSecret = function (consumerSecret) { - Meteor.accounts.twitter._secret = consumerSecret; - }; - Meteor.accounts.oauth.registerService('twitter', 1, function(oauthBinding) { var identity = oauthBinding.get('https://api.twitter.com/1/account/verify_credentials.json'); diff --git a/packages/accounts-ui/login_buttons.html b/packages/accounts-ui/login_buttons.html index 5dc344b2ae..5538f45028 100644 --- a/packages/accounts-ui/login_buttons.html +++ b/packages/accounts-ui/login_buttons.html @@ -11,10 +11,12 @@
Logout
{{else}} {{#if services}} - {{#if dropdown}} - {{> loginButtonsServicesDropdown}} - {{else}} - {{> loginButtonsServicesRow}} + {{#if configurationLoaded}} + {{#if dropdown}} + {{> loginButtonsServicesDropdown}} + {{else}} + {{> loginButtonsServicesRow}} + {{/if}} {{/if}} {{else}}
No login services configured.
@@ -65,10 +67,17 @@ {{/if}} {{else}} -
-
- Sign in with {{name}} -
+ {{#if configured}} +
+
+ Sign in with {{name}} +
+ {{else}} +
+
+ Configure {{name}} Login +
+ {{/if}} {{/if}} {{/each}} @@ -176,8 +185,48 @@ {{/if}} + + {{> resetPasswordForm}} {{> enrollAccountForm}} {{> justValidatedUserForm}} + {{> configureLoginServicesDialog}} diff --git a/packages/accounts-ui/login_buttons.js b/packages/accounts-ui/login_buttons.js index 64be104617..da00f2068c 100644 --- a/packages/accounts-ui/login_buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -11,6 +11,10 @@ var RESET_PASSWORD_TOKEN_KEY = 'Meteor.loginButtons.resetPasswordToken'; var ENROLL_ACCOUNT_TOKEN_KEY = 'Meteor.loginButtons.enrollAccountToken'; var JUST_VALIDATED_USER_KEY = 'Meteor.loginButtons.justValidatedUser'; + var CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE = 'Meteor.loginButtons.configureLoginServicesDialogVisible'; + var CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME = "Meteor.loginButtons.configureLoginServicesDialogServiceName"; + var CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED = "Meteor.accounts.facebook.saveEnabled"; + var resetSession = function () { Session.set(IN_SIGNUP_FLOW_KEY, false); @@ -29,57 +33,43 @@ // loginButtons template // + configureService = function(name) { + Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE, true); + Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME, name); + Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED, false); + }; + Template.loginButtons.events = { 'click #login-buttons-Facebook': function () { - try { + if (Meteor.accounts.configuration.findOne({service: 'facebook'})) { Meteor.loginWithFacebook(); - } catch (e) { - if (e instanceof Meteor.accounts.ConfigError) - alert("Facebook API key not set. Configure app details with " - + "Meteor.accounts.facebook.config() " - + "and Meteor.accounts.facebook.setSecret()"); - else - throw e; + } else { + configureService("Facebook"); // XXX refactor "Facebook" -> "facebook" } }, 'click #login-buttons-Google': function () { - try { + if (Meteor.accounts.configuration.findOne({service: 'google'})) { Meteor.loginWithGoogle(); - } catch (e) { - if (e instanceof Meteor.accounts.ConfigError) - alert("Google API key not set. Configure app details with " - + "Meteor.accounts.google.config() and " - + "Meteor.accounts.google.setSecret()"); - else - throw e; - }; + } else { + configureService("Google"); + } }, 'click #login-buttons-Weibo': function () { - try { + if (Meteor.accounts.configuration.findOne({service: 'weibo'})) { Meteor.loginWithWeibo(); - } catch (e) { - if (e instanceof Meteor.accounts.ConfigError) - alert("Weibo API key not set. Configure app details with " - + "Meteor.accounts.weibo.config() and " - + "Meteor.accounts.weibo.setSecret()"); - else - throw e; - }; + } else { + configureService("Weibo"); + } }, 'click #login-buttons-Twitter': function () { - try { + if (Meteor.accounts.configuration.findOne({service: 'twitter'})) { Meteor.loginWithTwitter(); - } catch (e) { - if (e instanceof Meteor.accounts.ConfigError) - alert("Twitter API key not set. Configure app details with " - + "Meteor.accounts.twitter.config() and " - + "Meteor.accounts.twitter.setSecret()"); - else - throw e; - }; + } else { + configureService("Twitter"); + } }, 'click #login-buttons-logout': function() { @@ -97,13 +87,17 @@ return service.name === 'Password'; }); - return hasPasswordService || services.length > 2; + return hasPasswordService || services.length > 1; }; Template.loginButtons.services = function () { return getLoginServices(); }; + Template.loginButtons.configurationLoaded = function () { + return Meteor.accounts.loginServicesConfigured(); + }; + Template.loginButtons.displayName = function () { var user = Meteor.user(); if (!user) @@ -260,6 +254,10 @@ || !Meteor.accounts._options.requireUsername; }; + Template.loginButtonsServicesRow.configured = function () { + return !!Meteor.accounts.configuration.findOne({service: this.name.toLowerCase()}); + }; + // // loginButtonsMessage template @@ -329,7 +327,7 @@ }, 'click .login-close-text': function () { resetSession(); - } + } }; Template.loginButtonsServicesDropdown.dropdownVisible = function () { @@ -456,6 +454,93 @@ } }); + // + // configureLoginServicesDialog template + // + + Template.configureLoginServicesDialog.events({ + 'click #configure-login-services-dismiss-button': function () { + Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE, false); + }, + 'click #configure-login-services-dialog-save-configuration': function () { + if (Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED)) { + // Prepare the configuration document for this login service + var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME).toLowerCase(); + var configuration = { + service: serviceName + }; + _.each(configurationFields(), function(field) { + configuration[field.property] = document.getElementById( + 'configure-login-services-dialog-' + field.property).value; + }); + + // Configure this login service + Meteor.call("configureLoginService", configuration, function (error, result) { + if (error) + Meteor._debug("Error configurating login service " + serviceName, error); + else + Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE, false); + }); + } + } + }); + + Template.configureLoginServicesDialog.events({ + 'input': function (event) { + // if the event fired on one of the configuration input fields, + // check whether we should enable the 'save configuration' button + if (event.target.id.indexOf('configure-login-services-dialog') === 0) + updateSaveDisabled(); + } + }); + + // check whether the 'save configuration' button should be enabled. + // this is a really strange way to implement this and a Forms + // Abstraction would make all of this reactive, and simpler. + var updateSaveDisabled = function () { + var saveEnabled = true; + _.any(configurationFields(), function(field) { + if (document.getElementById( + 'configure-login-services-dialog-' + field.property).value === '') { + saveEnabled = false; + return true; + } else { + return false; + } + }); + + Session.set(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED, saveEnabled); + }; + + // Returns the appropriate template for this login service. This + // template should be defined in the service's package + var configureLoginServicesDialogTemplateForService = function () { + var serviceName = Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SERVICE_NAME); + return Template['configureLoginServicesDialogFor' + serviceName]; + }; + + var configurationFields = function () { + var template = configureLoginServicesDialogTemplateForService(); + return template.fields(); + }; + + Template.configureLoginServicesDialog.configurationFields = function () { + return configurationFields(); + }; + + Template.configureLoginServicesDialog.visible = function () { + return Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_VISIBLE); + }; + + Template.configureLoginServicesDialog.configurationSteps = function () { + // renders the appropriate template + return configureLoginServicesDialogTemplateForService()(); + }; + + Template.configureLoginServicesDialog.saveDisabled = function () { + return !Session.get(CONFIGURE_LOGIN_SERVICES_DIALOG_SAVE_ENABLED); + }; + // // helpers // diff --git a/packages/accounts-ui/login_buttons.less b/packages/accounts-ui/login_buttons.less index e16371f657..9403ca3cb6 100644 --- a/packages/accounts-ui/login_buttons.less +++ b/packages/accounts-ui/login_buttons.less @@ -15,7 +15,7 @@ margin-left: 4px; } -@login-buttons-accounts-dialog-width: 158px; +@login-buttons-accounts-dialog-width: 178px; #login-buttons .login-button, .accounts-dialog .login-button { float: left; @@ -41,12 +41,20 @@ -o-border-radius: 3px; } +#login-buttons .login-button-disabled, .accounts-dialog .login-button-disabled { + color: #ccc; +} + +#login-buttons .configure-button { + background: red; +} + #login-buttons .login-link-text { margin-left: 5px; /* so that other elements aren't too close */ } .accounts-dialog .login-button { - width: 158px; + width: @login-buttons-accounts-dialog-width; margin-bottom: 4px; } @@ -106,7 +114,7 @@ padding-left: @login-buttons-accounts-dialog-padding-left; padding-bottom: 8px; - width: 167px; + width: @login-buttons-accounts-dialog-width + 9; /* not sure what this 9 is */ } #login-dropdown-list { @@ -137,7 +145,7 @@ } .accounts-dialog input { - width: 162px; + width: @login-buttons-accounts-dialog-width + 4; } .accounts-dialog .login-button-form-submit { @@ -177,7 +185,7 @@ float: left; } -#enroll-account-form, #reset-password-form, #just-validated-user-form { +#enroll-account-form, #reset-password-form, #just-validated-user-form, #configure-login-services-dialog { z-index: 1000; position: fixed; @@ -189,6 +197,18 @@ margin-top: -40px; /* = approximately -height/2, though height can change */ } +@configure-login-services-dialog-width: 530px; +#configure-login-services-dialog { + width: @configure-login-services-dialog-width; + margin-left: -(@configure-login-services-dialog-width + + @login-buttons-accounts-dialog-padding-left) / 2; + margin-top: -180px; /* = approximately -height/2, though height can change */ +} + +#configure-login-services-dialog .login-button-configure { + float: right; +} + #just-validated-dismiss-button { margin-top: 4px; } @@ -206,3 +226,39 @@ background-color: rgba(0, 0, 0, 0.7); } + +#configure-login-services-dialog table { + width: 100%; +} + +#configure-login-services-dialog .configuration_labels { + width: 30%; +} + +#configure-login-services-dialog .configuration_inputs { + width: 70%; +} + +#configure-login-services-dialog input { + width: 100%; + font-family: "Courier New", Courier, monospace; +} + +#configure-login-services-dialog ol { + margin-top: 10px; + margin-bottom: 10px; +} + +#configure-login-services-dialog .new-section { + margin-top: 10px; +} + +#configure-login-services-dialog ol li { + margin-left: 30px; +} + +#configure-login-services-dialog .url { + font-family: "Courier New", Courier, monospace; +} + + diff --git a/packages/accounts-weibo/package.js b/packages/accounts-weibo/package.js index d1cad4e758..c178954131 100644 --- a/packages/accounts-weibo/package.js +++ b/packages/accounts-weibo/package.js @@ -6,6 +6,11 @@ Package.on_use(function(api) { api.use('accounts-base', ['client', 'server']); api.use('accounts-oauth2-helper', ['client', 'server']); api.use('http', ['client', 'server']); + api.use('templating', 'client'); + + api.add_files( + ['weibo_configure.html', 'weibo_configure.js'], + 'client'); api.add_files('weibo_common.js', ['client', 'server']); api.add_files('weibo_server.js', 'server'); diff --git a/packages/accounts-weibo/weibo_client.js b/packages/accounts-weibo/weibo_client.js index 9371dc08cd..c9dcf36b0e 100644 --- a/packages/accounts-weibo/weibo_client.js +++ b/packages/accounts-weibo/weibo_client.js @@ -1,15 +1,16 @@ (function () { Meteor.loginWithWeibo = function () { - if (!Meteor.accounts.weibo._clientId || !Meteor.accounts.weibo._appUrl) - throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.weibo.config first"); + var config = Meteor.accounts.configuration.findOne({service: 'weibo'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); var state = Meteor.uuid(); // XXX need to support configuring access_type and scope var loginUrl = 'https://api.weibo.com/oauth2/authorize' + '?response_type=code' + - '&client_id=' + Meteor.accounts.weibo._clientId + - '&redirect_uri=' + Meteor.accounts.weibo._appUrl + '/_oauth/weibo?close' + + '&client_id=' + config.clientId + + '&redirect_uri=' + Meteor.absoluteUrl('_oauth/weibo?close', {replaceLocalhost: true}) + '&state=' + state; Meteor.accounts.oauth.initiateLogin(state, loginUrl); diff --git a/packages/accounts-weibo/weibo_configure.html b/packages/accounts-weibo/weibo_configure.html new file mode 100644 index 0000000000..a509880207 --- /dev/null +++ b/packages/accounts-weibo/weibo_configure.html @@ -0,0 +1,25 @@ + diff --git a/packages/accounts-weibo/weibo_configure.js b/packages/accounts-weibo/weibo_configure.js new file mode 100644 index 0000000000..1f13efdce7 --- /dev/null +++ b/packages/accounts-weibo/weibo_configure.js @@ -0,0 +1,11 @@ +Template.configureLoginServicesDialogForWeibo.siteUrl = function () { + // Weibo doesn't recognize localhost as a domain + return Meteor.absoluteUrl({replaceLocalhost: true}); +}; + +Template.configureLoginServicesDialogForWeibo.fields = function () { + return [ + {property: 'clientId', label: 'App Key'}, + {property: 'secret', label: 'App Secret'} + ]; +}; \ No newline at end of file diff --git a/packages/accounts-weibo/weibo_server.js b/packages/accounts-weibo/weibo_server.js index 822ea48c44..1c95214001 100644 --- a/packages/accounts-weibo/weibo_server.js +++ b/packages/accounts-weibo/weibo_server.js @@ -1,9 +1,5 @@ (function () { - Meteor.accounts.weibo.setSecret = function (secret) { - Meteor.accounts.weibo._secret = secret; - }; - Meteor.accounts.oauth.registerService('weibo', 2, function(query) { var accessToken = getAccessToken(query); @@ -24,12 +20,16 @@ }); var getAccessToken = function (query) { + var config = Meteor.accounts.configuration.findOne({service: 'weibo'}); + if (!config) + throw new Meteor.accounts.ConfigError("Service not configured"); + var result = Meteor.http.post( "https://api.weibo.com/oauth2/access_token", {params: { code: query.code, - client_id: Meteor.accounts.weibo._clientId, - client_secret: Meteor.accounts.weibo._secret, - redirect_uri: Meteor.accounts.weibo._appUrl + "/_oauth/weibo?close", + client_id: config.clientId, + client_secret: config.secret, + redirect_uri: Meteor.absoluteUrl("_oauth/weibo?close", {replaceLocalhost: true}), grant_type: 'authorization_code' }});