mirror of
https://github.com/meteor/meteor.git
synced 2026-05-02 03:01:46 -04:00
First pass at an account system backed by Facebook using OAuth
This commit is contained in:
committed by
Nick Martin
parent
301a339eef
commit
f8a0d5a95c
2
.gitignore
vendored
2
.gitignore
vendored
@@ -3,3 +3,5 @@
|
||||
/dev_bundle
|
||||
/dev_bundle*.tar.gz
|
||||
/dist
|
||||
\#*\#
|
||||
.\#*
|
||||
43
packages/accounts/accounts_client.js
Normal file
43
packages/accounts/accounts_client.js
Normal file
@@ -0,0 +1,43 @@
|
||||
Meteor.loginWithFacebook = function () {
|
||||
var openCenteredPopup = function(url, width, height) {
|
||||
var screenX = typeof window.screenX !== 'undefined'
|
||||
? window.screenX : window.screenLeft;
|
||||
var screenY = typeof window.screenY !== 'undefined'
|
||||
? window.screenY : window.screenTop;
|
||||
var outerWidth = typeof window.outerWidth !== 'undefined'
|
||||
? window.outerWidth : document.body.clientWidth;
|
||||
var outerHeight = typeof window.outerHeight !== 'undefined'
|
||||
? window.outerHeight : (document.body.clientHeight - 22);
|
||||
|
||||
// Use `outerWidth - width` and `outerHeight - height` for help in
|
||||
// positioning the popup centered relative to the current window
|
||||
var left = screenX + (outerWidth - width) / 2;
|
||||
var top = screenY + (outerHeight - height) / 2;
|
||||
var features = ('width=' + width + ',height=' + height +
|
||||
',left=' + left + ',top=' + top);
|
||||
|
||||
var newwindow = window.open(url, 'Login', features);
|
||||
if (newwindow.focus)
|
||||
newwindow.focus();
|
||||
};
|
||||
|
||||
if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
|
||||
throw new Error("Need to call Meteor.accounts.facebook.setup first");
|
||||
|
||||
var oauthState = Meteor.uuid();
|
||||
|
||||
openCenteredPopup(
|
||||
'https://www.facebook.com/dialog/oauth?client_id=' + Meteor.accounts.facebook._appId +
|
||||
'&redirect_uri=' + Meteor.accounts.facebook._appUrl + '/_oauth/facebook' +
|
||||
'&scope=email&state=' + oauthState,
|
||||
1000, 600); // XXX should we use different dimensions, e.g. on mobile?
|
||||
|
||||
Meteor.apply('login', [
|
||||
{oauth: {version: 2, provider: 'facebook', state: oauthState}}
|
||||
], {wait: true}, function(error, result) {
|
||||
Meteor.default_connection.setUserId(result.id);
|
||||
Meteor.default_connection.onReconnect = function() {
|
||||
Meteor.apply('login', [{resume: result.token}], {wait: true});
|
||||
};
|
||||
});
|
||||
};
|
||||
16
packages/accounts/accounts_common.js
Normal file
16
packages/accounts/accounts_common.js
Normal file
@@ -0,0 +1,16 @@
|
||||
Meteor.users = new Meteor.Collection("users");
|
||||
|
||||
if (!Meteor.accounts) {
|
||||
Meteor.accounts = {};
|
||||
}
|
||||
|
||||
if (!Meteor.accounts.facebook) {
|
||||
Meteor.accounts.facebook = {};
|
||||
}
|
||||
|
||||
Meteor.accounts._loginTokens = new Meteor.Collection("accounts._loginTokens");
|
||||
|
||||
Meteor.accounts.facebook.setup = function(appId, appUrl) {
|
||||
Meteor.accounts.facebook._appId = appId;
|
||||
Meteor.accounts.facebook._appUrl = appUrl;
|
||||
};
|
||||
170
packages/accounts/accounts_server.js
Normal file
170
packages/accounts/accounts_server.js
Normal file
@@ -0,0 +1,170 @@
|
||||
(function() {
|
||||
|
||||
var connect = __meteor_bootstrap__.require("connect");
|
||||
|
||||
// A map from oauth "state"s to `Future`s on which calling `return`
|
||||
// will unblock the corresponding outstanding call to `login`
|
||||
var oauthFutures = {};
|
||||
|
||||
// A map from oauth "state"s to incoming requests that, when processed,
|
||||
// had no matching future (presumably because the login popup window
|
||||
// finished its work before the server executed the call to `login`)
|
||||
var unmatchedOauthRequests = {};
|
||||
|
||||
// XXX add test for supporting both: first receiving the oauth request
|
||||
// and then executing call to `login`; and vice versa
|
||||
|
||||
Meteor.accounts.facebook.setSecret = function(secret) {
|
||||
Meteor.accounts.facebook._secret = secret;
|
||||
};
|
||||
|
||||
// Listen on /_oauth/*
|
||||
__meteor_bootstrap__.app
|
||||
.use(connect.query())
|
||||
.use(function (req, res, next) {
|
||||
Fiber(function() {
|
||||
// Any non-oauth request will continue down the default middlewares
|
||||
if (req.url.split('/')[1] !== '_oauth')
|
||||
next();
|
||||
|
||||
if (!Meteor.accounts.facebook._appId || !Meteor.accounts.facebook._appUrl)
|
||||
throw new Error("Need to call Meteor.accounts.facebook.setup first");
|
||||
if (!Meteor.accounts.facebook._secret)
|
||||
throw new Error("Need to call Meteor.accounts.facebook.setSecret first");
|
||||
|
||||
// Close the popup window
|
||||
res.writeHead(200, { 'Content-Type': 'text/html' });
|
||||
var content =
|
||||
'<html><head><script>window.close()</script></head></html>';
|
||||
res.end(content, 'utf-8');
|
||||
|
||||
// Try to unblock the appropriate call to `login`
|
||||
var future = oauthFutures[req.query.state];
|
||||
if (future) {
|
||||
// Unblock the `login` call
|
||||
future.return(handleOauthRequest(req));
|
||||
} else {
|
||||
// Store this request. We expect to soon get a call to `login`
|
||||
unmatchedOauthRequests[req.query.state] = req;
|
||||
}
|
||||
}).run();
|
||||
});
|
||||
|
||||
Meteor.methods({
|
||||
login: function(options) {
|
||||
// XXX write test for updateOrCreateUser
|
||||
var updateOrCreateUser = function(email, fbId, fbAccessToken) {
|
||||
var userByEmail = Meteor.users.findOne({emails: email});
|
||||
if (userByEmail) {
|
||||
var user = userByEmail;
|
||||
if (!user.services || !user.services.facebook)
|
||||
Meteor.users.update(user, {$set: {"services.facebook": {
|
||||
id: fbId,
|
||||
accessToken: fbAccessToken
|
||||
}}});
|
||||
return user._id;
|
||||
} else {
|
||||
var userByFacebookId = Meteor.users.findOne({"services.facebook.id": fbId});
|
||||
if (userByFacebookId) {
|
||||
var user = userByFacebookId;
|
||||
if (user.emails.indexOf(email) === -1) {
|
||||
// The user may have changed the email address associated with
|
||||
// their facebook account.
|
||||
Meteor.users.update(user, {$push: {emails: email}});
|
||||
}
|
||||
return user._id;
|
||||
} else {
|
||||
return Meteor.users.insert({
|
||||
emails: [email],
|
||||
services: {
|
||||
facebook: {id: fbId, accessToken: fbAccessToken}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
if (options.oauth) {
|
||||
if (options.oauth.version !== 2 || options.oauth.provider !== 'facebook')
|
||||
throw new Error("We only support facebook login for now. More soon!");
|
||||
|
||||
var fbAccessToken;
|
||||
if (unmatchedOauthRequests[options.oauth.state]) {
|
||||
// We had previously received the HTTP request with the OAuth code
|
||||
fbAccessToken = handleOauthRequest(
|
||||
unmatchedOauthRequests[options.oauth.state]);
|
||||
delete unmatchedOauthRequests[options.oauth.state];
|
||||
} else {
|
||||
if (oauthFutures[options.oauth.state])
|
||||
throw new Error("STRANGE! We are trying to set up a future for this OAuth state twice " +
|
||||
"(this could happen if one calls login twice without waiting). " +
|
||||
options.oauth.state);
|
||||
|
||||
// Prepare Future that will be `return`ed when we get an incoming
|
||||
// HTTP request with the OAuth code
|
||||
oauthFutures[options.oauth.state] = new Future;
|
||||
fbAccessToken = oauthFutures[options.oauth.state].wait();
|
||||
delete oauthFutures[options.oauth.state];
|
||||
}
|
||||
|
||||
// Fetch user's facebook identity
|
||||
var identity = Meteor.http.get(
|
||||
"https://graph.facebook.com/me?access_token=" + fbAccessToken).data;
|
||||
this.setUserId(updateOrCreateUser(identity.email, identity.id, fbAccessToken));
|
||||
|
||||
// Generate and store a login token for reconnect
|
||||
var loginToken = Meteor.accounts._loginTokens.insert({
|
||||
userId: this.userId()
|
||||
});
|
||||
|
||||
return {
|
||||
token: loginToken,
|
||||
id: this.userId()
|
||||
};
|
||||
} else 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);
|
||||
|
||||
// XXX do we need to actually return this here?
|
||||
return {
|
||||
token: loginToken,
|
||||
id: this.userId()
|
||||
};
|
||||
} else {
|
||||
throw new Error("Unrecognized options for login request");
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
// @returns {String} Facebook access token
|
||||
var handleOauthRequest = function(req) {
|
||||
var bareUrl = req.url.substring(0, req.url.indexOf('?'));
|
||||
var provider = bareUrl.split('/')[2];
|
||||
if (provider === 'facebook') {
|
||||
// Request an access token
|
||||
var response = Meteor.http.get(
|
||||
"https://graph.facebook.com/oauth/access_token?" +
|
||||
"client_id=" + Meteor.accounts.facebook._appId +
|
||||
// XXX what does this redirect_uri even mean?
|
||||
"&redirect_uri=" + Meteor.accounts.facebook._appUrl + "/_oauth/facebook" +
|
||||
"&client_secret=" + Meteor.accounts.facebook._secret +
|
||||
"&code=" + req.query.code).content;
|
||||
|
||||
// Extract the facebook access token from the response
|
||||
var fbAccessToken;
|
||||
_.each(response.split('&'), function(kvString) {
|
||||
var kvArray = kvString.split('=');
|
||||
if (kvArray[0] === 'access_token')
|
||||
fbAccessToken = kvArray[1];
|
||||
// XXX also parse the "expires" argument?
|
||||
});
|
||||
|
||||
return fbAccessToken;
|
||||
} else {
|
||||
throw new Error("Unknown OAuth provider: " + provider);
|
||||
}
|
||||
};
|
||||
})();
|
||||
|
||||
9
packages/accounts/package.js
Normal file
9
packages/accounts/package.js
Normal file
@@ -0,0 +1,9 @@
|
||||
Package.describe({
|
||||
summary: "A user account system",
|
||||
});
|
||||
|
||||
Package.on_use(function(api) {
|
||||
api.add_files('accounts_common.js', ['client', 'server']);
|
||||
api.add_files('accounts_server.js', 'server');
|
||||
api.add_files('accounts_client.js', 'client');
|
||||
});
|
||||
Reference in New Issue
Block a user