mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
A variety of improvements to the accounts packages
This commit is contained in:
committed by
Nick Martin
parent
a73715491b
commit
aeb733b3e3
@@ -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,
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
})();
|
||||
})();
|
||||
|
||||
@@ -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)
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
}
|
||||
})();
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
};
|
||||
}) ();
|
||||
|
||||
|
||||
@@ -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];
|
||||
}
|
||||
};
|
||||
};
|
||||
})();
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
});
|
||||
};
|
||||
})();
|
||||
@@ -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();
|
||||
});
|
||||
|
||||
@@ -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']);
|
||||
|
||||
Reference in New Issue
Block a user