diff --git a/examples/todos/server/publish.js b/examples/todos/server/publish.js index 151241cfd1..1552c5e4ef 100644 --- a/examples/todos/server/publish.js +++ b/examples/todos/server/publish.js @@ -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, diff --git a/packages/accounts-facebook/facebook_client.js b/packages/accounts-facebook/facebook_client.js index 2933a9b5d3..155dd7949a 100644 --- a/packages/accounts-facebook/facebook_client.js +++ b/packages/accounts-facebook/facebook_client.js @@ -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. diff --git a/packages/accounts-facebook/facebook_server.js b/packages/accounts-facebook/facebook_server.js index 80e1c51d4e..4149e3bdc8 100644 --- a/packages/accounts-facebook/facebook_server.js +++ b/packages/accounts-facebook/facebook_server.js @@ -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; } }; diff --git a/packages/accounts-google/google_client.js b/packages/accounts-google/google_client.js index c09481e1cc..ab740171e3 100644 --- a/packages/accounts-google/google_client.js +++ b/packages/accounts-google/google_client.js @@ -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 diff --git a/packages/accounts-google/google_server.js b/packages/accounts-google/google_server.js index 9930ec2d4f..c80059a628 100644 --- a/packages/accounts-google/google_server.js +++ b/packages/accounts-google/google_server.js @@ -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; }; -})(); \ No newline at end of file +})(); diff --git a/packages/accounts-ui/login-buttons.css b/packages/accounts-ui/login_buttons.css similarity index 100% rename from packages/accounts-ui/login-buttons.css rename to packages/accounts-ui/login_buttons.css diff --git a/packages/accounts-ui/login-buttons.html b/packages/accounts-ui/login_buttons.html similarity index 100% rename from packages/accounts-ui/login-buttons.html rename to packages/accounts-ui/login_buttons.html diff --git a/packages/accounts-ui/login-buttons.js b/packages/accounts-ui/login_buttons.js similarity index 73% rename from packages/accounts-ui/login-buttons.js rename to packages/accounts-ui/login_buttons.js index 3412824d4c..f588e0fad2 100644 --- a/packages/accounts-ui/login-buttons.js +++ b/packages/accounts-ui/login_buttons.js @@ -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) diff --git a/packages/accounts-ui/login-buttons-images.css b/packages/accounts-ui/login_buttons_image.css similarity index 100% rename from packages/accounts-ui/login-buttons-images.css rename to packages/accounts-ui/login_buttons_image.css diff --git a/packages/accounts-ui/package.js b/packages/accounts-ui/package.js index f0d1c0d9d0..c0c17d4b36 100644 --- a/packages/accounts-ui/package.js +++ b/packages/accounts-ui/package.js @@ -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'); }); diff --git a/packages/accounts/accounts_client.js b/packages/accounts/accounts_client.js index 65b6835941..a0351d51ff 100644 --- a/packages/accounts/accounts_client.js +++ b/packages/accounts/accounts_client.js @@ -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(); + }); + } })(); diff --git a/packages/accounts/accounts_server.js b/packages/accounts/accounts_server.js index 89df66dbea..e38311ab37 100644 --- a/packages/accounts/accounts_server.js +++ b/packages/accounts/accounts_server.js @@ -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; - } - }; }) (); diff --git a/packages/localstorage-polyfill/localstorage_polyfill.js b/packages/localstorage-polyfill/localstorage_polyfill.js index c3b83d403b..561702b40a 100644 --- a/packages/localstorage-polyfill/localstorage_polyfill.js +++ b/packages/localstorage-polyfill/localstorage_polyfill.js @@ -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]; + } }; }; })(); diff --git a/packages/localstorage-polyfill/package.js b/packages/localstorage-polyfill/package.js index b065b1fa64..f6e269b2e2 100644 --- a/packages/localstorage-polyfill/package.js +++ b/packages/localstorage-polyfill/package.js @@ -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'); }); diff --git a/packages/oauth2/oauth2_client.js b/packages/oauth2/oauth2_client.js index a1d13f9580..f6acb7f775 100644 --- a/packages/oauth2/oauth2_client.js +++ b/packages/oauth2/oauth2_client.js @@ -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); - } - }); - }; })(); \ No newline at end of file diff --git a/packages/oauth2/oauth2_server.js b/packages/oauth2/oauth2_server.js index 12e45f9db6..b15b717ac0 100644 --- a/packages/oauth2/oauth2_server.js +++ b/packages/oauth2/oauth2_server.js @@ -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/?" + 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(); }); diff --git a/packages/oauth2/package.js b/packages/oauth2/package.js index 1a184ded51..3cab974f48 100644 --- a/packages/oauth2/package.js +++ b/packages/oauth2/package.js @@ -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']);