A variety of improvements to the accounts packages

This commit is contained in:
Avital Oliver
2012-06-15 15:38:18 -07:00
committed by Nick Martin
parent a73715491b
commit aeb733b3e3
17 changed files with 170 additions and 131 deletions

View File

@@ -14,7 +14,7 @@ Meteor.publish('lists', function () {
// timestamp: Number}
Todos = new Meteor.Collection("todos");
// Publish all items for requested list_id.
// Publish visible items for requested list_id.
Meteor.publish('todos', function (list_id) {
return Todos.find({
list_id: list_id,

View File

@@ -1,7 +1,7 @@
(function () {
Meteor.loginWithFacebook = function () {
if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setup first");
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.config first");
var state = Meteor.uuid();
// XXX I think there's a smaller popup. Replace with appropriate URL.

View File

@@ -7,12 +7,11 @@
Meteor.accounts.oauth2.registerService('facebook', function(query) {
if (query.error) {
// The user didn't authorize access
// XXX can/should we generalize this into the oauth abstration?
return null;
}
if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setup first");
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.config first");
if (!Meteor.accounts.facebook._secret)
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.facebook.setSecret first");
@@ -29,7 +28,7 @@
var getAccessToken = function (query) {
// Request an access token
var response = Meteor.http.get(
var result = Meteor.http.get(
"https://graph.facebook.com/oauth/access_token", {
params: {
client_id: Meteor.accounts.facebook._appId,
@@ -37,9 +36,14 @@
client_secret: Meteor.accounts.facebook._secret,
code: query.code
}
}).content;
});
// Errors come back as JSON but success looks like a query encoded in a url
if (result.error)
throw result.error;
var response = result.content;
// Errors come back as JSON but success looks like a query encoded
// in a url
var error_response;
try {
// Just try to parse so that we know if we failed or not,
@@ -66,6 +70,8 @@
// XXX also parse the "expires" argument?
});
if (!fbAccessToken)
throw new Meteor.Error("Couldn't find access token in HTTP response: " + response);
return fbAccessToken;
}
};

View File

@@ -1,7 +1,7 @@
(function () {
Meteor.loginWithGoogle = function () {
if (!Meteor.accounts.google._clientId || !Meteor.accounts.google._appUrl)
throw new Meteor.accounts.google.SetupError("Need to call Meteor.accounts.google.setup first");
throw new Meteor.accounts.ConfigError("Need to call Meteor.accounts.google.config first");
var state = Meteor.uuid();
// XXX need to support configuring access_type and scope

View File

@@ -7,7 +7,6 @@
Meteor.accounts.oauth2.registerService('google', function(query) {
if (query.error) {
// The user didn't authorize access
// XXX can/should we generalize this into the oauth abstration?
return null;
}
@@ -53,4 +52,4 @@
throw result.error;
return result.data;
};
})();
})();

View File

@@ -6,7 +6,9 @@
Meteor.loginWithFacebook();
} catch (e) {
if (e instanceof Meteor.accounts.ConfigError)
alert("Facebook API key not set. Configure app details with Meteor.accounts.facebook.config()");
alert("Facebook API key not set. Configure app details with "
+ "Meteor.accounts.facebook.config() "
+ "and Meteor.accounts.facebook.setSecret()");
else
throw e;
}
@@ -17,7 +19,9 @@
Meteor.loginWithGoogle();
} catch (e) {
if (e instanceof Meteor.accounts.ConfigError)
alert("Google API key not set. Configure app details with Meteor.accounts.google.config()");
alert("Google API key not set. Configure app details with "
+ "Meteor.accounts.google.config() and "
+ "Meteor.accounts.google.setSecret()");
else
throw e;
};
@@ -30,6 +34,8 @@
Template.loginButtons.services = function () {
var ret = [];
// XXX It would be nice if there were an automated way to read the
// list of services, such as _.each(Meteor.accounts.services, ...)
if (Meteor.accounts.facebook)
ret.push({name: 'Facebook'});
if (Meteor.accounts.google)
@@ -38,13 +44,6 @@
return ret;
};
Template.loginButtons.userEmail = function () {
var user = Meteor.user();
if (!user || !user.emails || !user.emails[0])
return '';
return user.emails[0];
};
Template.loginButtons.userName = function () {
var user = Meteor.user();
if (!user || !user.name)

View File

@@ -6,8 +6,8 @@ Package.on_use(function (api) {
api.use(['accounts', 'underscore', 'liveui', 'templating'], 'client');
api.add_files([
'login-buttons.css',
'login-buttons-images.css',
'login-buttons.html',
'login-buttons.js'], 'client');
'login_buttons.css',
'login_buttons_images.css',
'login_buttons.html',
'login_buttons.js'], 'client');
});

View File

@@ -1,7 +1,6 @@
(function () {
Meteor.user = function () {
var userId = Meteor.default_connection.userId();
if (userId) {
var result = Meteor.users.findOne(userId);
@@ -18,12 +17,6 @@
}
};
if (Handlebars) {
Handlebars.registerHelper('currentUser', function () {
return Meteor.user();
});
}
Meteor.logout = function () {
Meteor.apply('logout', [], {wait: true}, function(error, result) {
if (error)
@@ -33,4 +26,11 @@
});
};
// If we're using Handlebars, register the {{currentUser}} global
// helper
if (Handlebars) {
Handlebars.registerHelper('currentUser', function () {
return Meteor.user();
});
}
})();

View File

@@ -1,4 +1,83 @@
(function () {
///
/// LOGIN HANDLERS
///
Meteor.methods({
// @returns {Object|null}
// If successful, returns {token: reconnectToken, id: userId}
// If unsuccessful (for example, if the user closed the oauth login popup),
// returns null
login: function(options) {
var result = tryAllLoginHandlers(options);
if (result !== null)
this.setUserId(result.id);
return result;
},
logout: function() {
this.setUserId(null);
}
});
Meteor.accounts._loginHandlers = [];
// Try all of the registered login handlers until one of them
// doesn't return `undefined` (NOT null), meaning it handled this
// call to `login`. Return that return value.
var tryAllLoginHandlers = function (options) {
var result = undefined;
_.find(Meteor.accounts._loginHandlers, function(handler) {
var maybeResult = handler(options);
if (maybeResult !== undefined) {
result = maybeResult;
return true;
} else {
return false;
}
});
if (result === undefined) {
throw new Meteor.Error("Unrecognized options for login request");
} else {
return result;
}
};
// @param handler {Function} A function that receives an options object
// (as passed as an argument to the `login` method) and returns one of:
// - `undefined`, meaning don't handle;
// - `null`, meaning the user didn't actually log in;
// - {id: userId, accessToken: *}, if the user logged in successfully.
Meteor.accounts.registerLoginHandler = function(handler) {
Meteor.accounts._loginHandlers.push(handler);
};
// support reconnecting using a meteor login token
Meteor.accounts.registerLoginHandler(function(options) {
if (options.resume) {
var loginToken = Meteor.accounts._loginTokens
.findOne({_id: options.resume});
if (!loginToken)
throw new Meteor.Error("Couldn't find login token");
this.setUserId(loginToken.userId);
return {
token: loginToken,
id: this.userId()
};
} else {
return undefined;
}
});
///
/// MANAGING USER OBJECTS
///
// Updates or creates a user after we authenticate with a 3rd party
//
// @param email {String} The user's email
@@ -69,47 +148,10 @@
}
};
Meteor.accounts._loginHandlers = [];
// @param handler {Function} A function that receives an options object
// (as passed as an argument to the `login` method) and returns one of:
// - `undefined`, meaning don't handle;
// - `null`, meaning the user didn't actually log in;
// - {id: userId, accessToken: *}, if the user logged in successfully.
Meteor.accounts.registerLoginHandler = function(handler) {
Meteor.accounts._loginHandlers.push(handler);
};
Meteor.methods({
// @returns {Object|null}
// If successful, returns {token: reconnectToken, id: userId}
// If unsuccessful (for example, if the user closed the oauth login popup),
// returns null
login: function(options) {
if (options.resume) {
var loginToken = Meteor.accounts._loginTokens
.findOne({_id: options.resume});
if (!loginToken)
throw new Meteor.Error("Couldn't find login token");
this.setUserId(loginToken.userId);
return {
token: loginToken,
id: this.userId()
};
} else {
var result = tryAllLoginHandlers(options);
if (result !== null)
this.setUserId(result.id);
return result;
}
},
logout: function() {
this.setUserId(null);
}
});
///
/// PUBLISHING USER OBJECTS
///
// Always publish the current user's record to the client.
Meteor.publish(null, function() {
@@ -128,30 +170,5 @@
};
Meteor.default_server.publish(null, handler, {is_auto: true});
});
// Try all of the registered login handlers until one of them doesn't
// return `undefined`, meaning it handled this call to `login`. Return
// that return value.
var tryAllLoginHandlers = function (options) {
var result = undefined;
_.find(Meteor.accounts._loginHandlers, function(handler) {
var maybeResult = handler(options);
if (maybeResult !== undefined) {
result = maybeResult;
return true;
} else {
return false;
}
});
if (result === undefined) {
throw new Meteor.Error("Unrecognized options for login request");
} else {
return result;
}
};
}) ();

View File

@@ -35,9 +35,17 @@ Meteor.startup(function() { // Since we need document.body to be defined
+ "tab to be logged in.");
return {
setItem: function () {},
removeItem: function () {},
getItem: function () {}
_data: {},
setItem: function (key, val) {
this._data[key] = val;
},
removeItem: function (key) {
delete this._data[key];
},
getItem: function (key) {
return this._data[key];
}
};
};
})();

View File

@@ -3,6 +3,8 @@ Package.describe({
});
Package.on_use(function (api) {
api.use('jquery', 'client'); // XXX only used for browser detection. remove.
api.add_files('localstorage_polyfill.js', 'client');
});

View File

@@ -1,4 +1,8 @@
(function () {
// Open a popup window pointing to a OAuth handshake page
//
// @param state {String} The OAuth state generated by the client
// @param url {String} url to page
Meteor.accounts.oauth2.initiateLogin = function(state, url) {
// XXX should we use different dimensions, e.g. on mobile?
var popup = openCenteredPopup(url, 1000, 600);
@@ -11,6 +15,26 @@
}, 100);
};
// Send an OAuth login method to the server. If the user authorized
// access in the popup this should log the user in, otherwise
// nothing should happen.
var tryLoginAfterPopupClosed = function(state) {
Meteor.apply('login', [
{oauth: {version: 2, state: state}}
], {wait: true}, function(error, result) {
if (error)
throw error;
if (!result) {
// The user either closed the OAuth popup or didn't authorize
// access. Do nothing.
return;
} else {
Meteor.accounts.loginAndStoreToken(result.token);
}
});
};
var openCenteredPopup = function(url, width, height) {
var screenX = typeof window.screenX !== 'undefined'
? window.screenX : window.screenLeft;
@@ -33,24 +57,4 @@
newwindow.focus();
return newwindow;
};
// Send an OAuth login method to the server. If the user authorized
// access in the popup this should log the user in, otherwise
// nothing should happen.
var tryLoginAfterPopupClosed = function(oauthState) {
Meteor.apply('login', [
{oauth: {version: 2, state: oauthState}}
], {wait: true}, function(error, result) {
if (error)
throw error;
if (!result) {
// The user either closed the OAuth popup or didn't authorize
// access. Do nothing.
return;
} else {
Meteor.accounts.loginAndStoreToken(result.token);
}
});
};
})();

View File

@@ -22,6 +22,7 @@
Meteor.accounts.oauth2.registerService = function (name, handleOauthRequest) {
if (Meteor.accounts.oauth2._services[name])
throw new Meteor.Error("Already registered the " + name + " OAuth2 service");
Meteor.accounts.oauth2._services[name] = {
handleOauthRequest: handleOauthRequest
};
@@ -40,23 +41,26 @@
return result;
});
// When we get an incoming OAuth http request we complete the
// facebook handshake, account and token setup before responding.
// The results are stored in this map which is then read when the
// login method is called. Maps {oauthState} --> return value of
// `login`
// When we get an incoming OAuth http request we complete the oauth
// handshake, account and token setup before responding. The
// results are stored in this map which is then read when the login
// method is called. Maps state --> return value of `login`
//
// XXX we should periodically clear old entries
Meteor.accounts.oauth2._loginResultForState = {};
// Listen on /_oauth/*
__meteor_bootstrap__.app
.use(connect.query())
.use(function (req, res, next) {
// Need to create a Fiber since we're using synchronous http calls
Fiber(function() {
var bareUrl = req.url.substring(0, req.url.indexOf('?'));
var splitUrl = bareUrl.split('/');
// req.url will be "/_oauth/<service name>?<action>"
var barePath = req.url.substring(0, req.url.indexOf('?'));
var splitPath = barePath.split('/');
// Any non-oauth request will continue down the default middlewares
if (splitUrl[1] !== '_oauth') {
if (splitPath[1] !== '_oauth') {
next();
return;
}
@@ -65,7 +69,7 @@
// This way the subsequent call to the `login` method will be
// immediate.
var serviceName = splitUrl[2];
var serviceName = splitPath[2];
var service = Meteor.accounts.oauth2._services[serviceName];
// Get or create user id
@@ -80,8 +84,8 @@
Meteor.accounts.oauth2._loginResultForState[req.query.state] =
{token: loginToken, id: userId};
// We support /_oauth?close, /_oauth?redirect=URL. Any other /_oauth request
// just served a blank page
// We support ?close and ?redirect=URL. Any other query should
// just serve a blank page
if ('close' in req.query) { // check with 'in' because we don't set a value
// Close the popup window
res.writeHead(200, {'Content-Type': 'text/html'});
@@ -93,7 +97,7 @@
res.end();
} else {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end(content, 'utf-8');
res.end('', 'utf-8');
}
}).run();
});

View File

@@ -1,9 +1,9 @@
Package.describe({
summary: "A basis for OAuth2-based account systems",
internal: true
});
Package.on_use(function (api) {
api.use('jquery', 'client'); // XXX only used for browser detection. remove.
api.use('accounts', ['client', 'server']);
api.add_files('oauth2_common.js', ['client', 'server']);