mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
New login service configuration system + UI.
Instead of configuring client ids and app secret from code, we store those in Mongo documents. This means it's easier to work with multiple deployments of the same app. Also, this allows for a wizard-style UI to configure your login services.
This commit is contained in:
committed by
Nick Martin
parent
c50cd1defa
commit
b01cb66029
@@ -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;
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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';
|
||||
@@ -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
|
||||
///
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
|
||||
19
packages/accounts-facebook/facebook_configure.html
Normal file
19
packages/accounts-facebook/facebook_configure.html
Normal file
@@ -0,0 +1,19 @@
|
||||
<template name="configureLoginServicesDialogForFacebook">
|
||||
<p>
|
||||
First, you'll need to register your app on Facebook. Follow these steps:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Visit <a href="https://developers.facebook.com/apps" target="_blank">https://developers.facebook.com/apps</a>
|
||||
</li>
|
||||
<li>
|
||||
Create New App (Only a name is required.)
|
||||
</li>
|
||||
<li>
|
||||
Under "Select how your app integrates with Facebook", expand "Website with Facebook Login".
|
||||
</li>
|
||||
<li>
|
||||
Set Site URL to: <span class="url">{{siteUrl}}</span>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
10
packages/accounts-facebook/facebook_configure.js
Normal file
10
packages/accounts-facebook/facebook_configure.js
Normal file
@@ -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'}
|
||||
];
|
||||
};
|
||||
@@ -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
|
||||
}
|
||||
});
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
28
packages/accounts-google/google_configure.html
Normal file
28
packages/accounts-google/google_configure.html
Normal file
@@ -0,0 +1,28 @@
|
||||
<template name="configureLoginServicesDialogForGoogle">
|
||||
<p>
|
||||
First, you'll need to get a Google Client ID. Follow these steps:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Visit <a href="https://code.google.com/apis/console/" target="blank">https://code.google.com/apis/console/</a>
|
||||
</li>
|
||||
<li>
|
||||
Open the "API Access" tab
|
||||
</li>
|
||||
<li>
|
||||
Create another Client ID
|
||||
</li>
|
||||
<li>
|
||||
Expand (more options)
|
||||
</li>
|
||||
<li>
|
||||
Set Authorized Redirect URIs to: <span class="url">{{siteUrl}}_oauth/google?close</span>
|
||||
</li>
|
||||
<li>
|
||||
Set Authorizes Javascript Origins to: <span class="url">{{siteUrl}}</span>
|
||||
</li>
|
||||
<li>
|
||||
Create client ID
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
10
packages/accounts-google/google_configure.js
Normal file
10
packages/accounts-google/google_configure.js
Normal file
@@ -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'}
|
||||
];
|
||||
};
|
||||
@@ -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'
|
||||
}});
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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",
|
||||
|
||||
13
packages/accounts-twitter/twitter_configure.html
Normal file
13
packages/accounts-twitter/twitter_configure.html
Normal file
@@ -0,0 +1,13 @@
|
||||
<template name="configureLoginServicesDialogForTwitter">
|
||||
<p>
|
||||
First, you'll need to register your app on Twitter. Follow these steps:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Visit <a href="https://dev.twitter.com/apps/new" target="_blank">https://dev.twitter.com/apps/new</a>
|
||||
</li>
|
||||
<li>
|
||||
Set Callback URL to: <span class="url">{{siteUrl}}_oauth/twitter?close</span>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
11
packages/accounts-twitter/twitter_configure.js
Normal file
11
packages/accounts-twitter/twitter_configure.js
Normal file
@@ -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'}
|
||||
];
|
||||
};
|
||||
@@ -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');
|
||||
|
||||
|
||||
@@ -11,10 +11,12 @@
|
||||
<div class="login-button" id="login-buttons-logout">Logout</div>
|
||||
{{else}}
|
||||
{{#if services}}
|
||||
{{#if dropdown}}
|
||||
{{> loginButtonsServicesDropdown}}
|
||||
{{else}}
|
||||
{{> loginButtonsServicesRow}}
|
||||
{{#if configurationLoaded}}
|
||||
{{#if dropdown}}
|
||||
{{> loginButtonsServicesDropdown}}
|
||||
{{else}}
|
||||
{{> loginButtonsServicesRow}}
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="no-services">No login services configured.</div>
|
||||
@@ -65,10 +67,17 @@
|
||||
</div>
|
||||
{{/if}}
|
||||
{{else}}
|
||||
<div class="login-button" id="login-buttons-{{name}}">
|
||||
<div class="login-image" id="login-buttons-image-{{name}}"></div>
|
||||
Sign in with {{name}}
|
||||
</div>
|
||||
{{#if configured}}
|
||||
<div class="login-button" id="login-buttons-{{name}}">
|
||||
<div class="login-image" id="login-buttons-image-{{name}}"></div>
|
||||
Sign in with {{name}}
|
||||
</div>
|
||||
{{else}}
|
||||
<div class="login-button configure-button" id="login-buttons-{{name}}">
|
||||
<div class="login-image" id="login-buttons-image-{{name}}"></div>
|
||||
Configure {{name}} Login
|
||||
</div>
|
||||
{{/if}}
|
||||
{{/if}}
|
||||
{{/each}}
|
||||
</template>
|
||||
@@ -176,8 +185,48 @@
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<template name="configureLoginServicesDialog">
|
||||
{{#if visible}}
|
||||
<div id="configure-login-services-dialog" class="accounts-dialog">
|
||||
{{{configurationSteps}}}
|
||||
|
||||
<p>
|
||||
Now, copy over some details.
|
||||
</p>
|
||||
<p>
|
||||
<table>
|
||||
<colgroup>
|
||||
<col span="1" class="configuration_labels">
|
||||
<col span="1" class="configuration_inputs">
|
||||
</colgroup>
|
||||
{{#each configurationFields}}
|
||||
<tr>
|
||||
<td>
|
||||
<label for="configure-login-services-dialog-{{property}}">{{label}}</label>
|
||||
</td>
|
||||
<td>
|
||||
<input id="configure-login-services-dialog-{{property}}" />
|
||||
</td>
|
||||
</tr>
|
||||
{{/each}}
|
||||
</table>
|
||||
</p>
|
||||
<div class="new-section">
|
||||
<div class="login-button" id="configure-login-services-dismiss-button">I'll do this later</div>
|
||||
{{#isolate}}
|
||||
<div class="login-button login-button-configure {{#if saveDisabled}}login-button-disabled{{/if}}"
|
||||
id="configure-login-services-dialog-save-configuration">
|
||||
Save Configuration
|
||||
</div>
|
||||
{{/isolate}}
|
||||
</div>
|
||||
</div>
|
||||
{{/if}}
|
||||
</template>
|
||||
|
||||
<body>
|
||||
{{> resetPasswordForm}}
|
||||
{{> enrollAccountForm}}
|
||||
{{> justValidatedUserForm}}
|
||||
{{> configureLoginServicesDialog}}
|
||||
</body>
|
||||
|
||||
@@ -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
|
||||
//
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -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);
|
||||
|
||||
25
packages/accounts-weibo/weibo_configure.html
Normal file
25
packages/accounts-weibo/weibo_configure.html
Normal file
@@ -0,0 +1,25 @@
|
||||
<template name="configureLoginServicesDialogForWeibo">
|
||||
<p>
|
||||
First, you'll need to register your app on Weibo. Follow these steps:
|
||||
</p>
|
||||
<ol>
|
||||
<li>
|
||||
Visit <a href="http://open.weibo.com/development" target="_blank">http://open.weibo.com/development</a>
|
||||
</li>
|
||||
<li>
|
||||
Click the "创建应用" button
|
||||
</li>
|
||||
<li>
|
||||
Select 网页应用在第三方网页内访问使用 (Web Applications)
|
||||
</li>
|
||||
<li>
|
||||
Complete the registration process
|
||||
</li>
|
||||
<li>
|
||||
Open 应用信息 (Application) -> 高级信息 (Senior Information)
|
||||
</li>
|
||||
<li>
|
||||
Set OAuth2.0 授权回调页 (authorized callback page) to: <span class="url">{{siteUrl}}_oauth/weibo?close</span>
|
||||
</li>
|
||||
</ol>
|
||||
</template>
|
||||
11
packages/accounts-weibo/weibo_configure.js
Normal file
11
packages/accounts-weibo/weibo_configure.js
Normal file
@@ -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'}
|
||||
];
|
||||
};
|
||||
@@ -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'
|
||||
}});
|
||||
|
||||
|
||||
Reference in New Issue
Block a user